This notebook is written with reticulate, a package that allows inter-operation between R and Python.


1 Motivation

Why do we need to explain a machine learning model? The benefit of an explanable model against a black-box model is for the model to be trusted. Trust can be important in many real applications where the successful deployment of a machine learning model requires the trust from end users. Sometimes trust plays a even bigger role than model accuracy.

Other than trust, model explainability (or interpretability, interchangeably used hereafter) may also guide us in the correct direction to further improve the model.1

In general, linear model is more interpretable than non-linear model. But the former also suffers from lower accuracy. More advanced and hence complicated model usually has worse interpretability.

One should not confuse model explainability with the actual causality. Being able to explain a model doesn’t mean that we can identify any ground-truth causal relation behind the model. Model explainability is for and only for the model, but not for the facts we’d like to model. Nevertheless, understand how we can reason the model definitely will help us better model the actual pattern behind the scence.

2 Open Source Libraries for Model Explanation

In this notebook we will walk through 3 popular approaches of model prediction explanation, each of them comes with a dedicated Python package:

  1. shap for SHAP
  2. lime for LIME
  3. interpret for Explainable Boosting Machine

All coding examples will be based on the above 3 packages. What if we’d like to explore these approaches in R? For shap, there is a R port called shapper. For lime we also have iml which contains more than just LIME approach. As for interpret, it already comes with its own R API.

3 Explanation Models

An explanation model \(g(x\prime)\) is an interpretable approximation of the original model \(f(x)\). Its sole purpose is to give extra explainability the original model fails to provide, due to its own complexity.

The general idea is to use a simplified input \(x\prime\) such that \(x = h_x(x\prime)\), where \(h_x(\cdot)\) is a mapping function for any given raw input \(x\). Then the interpretable approximation can be written as:

\[ g(x\prime) \approx f(h_x(x\prime)). \]

The additive feature attribution methods specify the explanation model of the following form:

\[ g(z\prime) = \phi_0 + \sum_{j = 1}^n \phi_i z_i\prime, \]

where \(n\) is total number of simplified features, \(z\prime \in \{0, 1\}\) simply an indicator. In many such methods, the simplified input is the indicator of feature presence. Apparently, the choice of an additive model is for (linear) intrepretability. The simplified features are an interpretable representation of the original model features.

As we will see additivity is the key to explainability. All the approaches we will discuss in this notebook follow this philosophy.

4 LIME

One very popular such above additive model is LIME (Ribeiro, Singh, and Guestrin (2016)). LIME stands for Local Interpretable Model-Agnostic Explanations. As its full name suggests, LIME can be applied to any machine learning model. LIME achieves prediction-level interpretability by approxmiating the original model with an explanation model locally around that prediction.

From their original paper:

By “explaining a prediction”, we mean presenting textual or visual artifacts that provide qualitative understanding of the relationship between the instance’s components (e.g. words in text, patches in an image) and the model’s prediction.

Feature space in the original model will be in general different from that of the explanation model. An explanation model will use interpretable representation of the original feature as their training input. Different data type will have their different interpretable representation.

4.1 Binarized Interpretable Feature Space

LIME proposes an explanation model \(g(x\prime)\) with a domain \(\{0, 1\}\). That is, it acts on absence or presence of the interpretable features \(x\prime\). The choice is, obviously, for better interpretability.

Language Data

For text classification problems with human language as source input, the most straightforward interpretable representation will be a binary indicator vector of bag of words. So the explanation model will try to reason which word or token is driving the prediction in what direction. And this is true no matter the form of the original model feature. May it be a word count matrix, a term frequency-inverse document frequency (TF-IDF) matrix, or numerical embeddings.

Image Data

For image tasks, the interpretable representation is a joint set of contiguous superpixels that divide the original image into pieces. A superpixel is a group of pixels with similar characteristics. So in plain words, just like we segment sentence into tokens, we simply segment image into multiple small pieces and again, use a binary vector to indicate the absence or presence of each piece for latter perturbation purpose.

Numerical Data

TBC.

4.2 Local Sampling (Perturbation)

In order to estimate the explanation model given a prediction for a target example, features transformed into a interpretable space are then perturbed to generate similar examples around that target example. This is referred to as sampling for local exploration.

Given an example \(x\prime\) to be explained, its non-zero interpretable features (remember the space has a domain of \(\{0, 1\}\)) are uniformly sampled to generate its local similar example \(z\prime\). So \(z\prime\) will always have a subset (or at most equal set) of non-zero features that \(x\prime\) has.

Take text data for illustration. If a particular example has the following tokens in the interpretable space:

A B C H I J

Then a possible local sample can be something like:

A C H J

(Imagine those alphabets are actual words present in the raw text of the example.)

By default in the paper 5,000 samples are generated for each single explanation. A hyperparameter \(K\) (default at 10) is used to cap how many non-zero interpretable features we’d like to estimate in the subsequent model learning phase, to not only keep the model solving tractable but also manageable for human interpretation.

4.3 Learning Task of the Explanation Model

Now each of the perturbed example will be first transformed back to their original feature space, then feed into the original model to get the predicted label. That is, from \(z\prime\) we need to get \(f(z)\) where \(f\) is the original model and \(z\) the original feature representation. These labels serve exactly as the labels to train the local explanation model, where all random perturbations \(z\) are weigthed by \(\pi_x(z)\), a proximity function \(\pi_x(z)\) can be defined to measure how close \(z\) is to \(x\), in the original space.

In the original paper the proximity function is set to be an exponential kernel:

\[ \pi_x(z) = \exp \bigg( \frac{-D(x, z)^2}{\sigma^2} \bigg), \]

where \(D(x, z)\) is cosine distance for text and L2 distance for image, \(\sigma\) is a hyperparameter default at 25 in lime.

The learner is a simple linear model:

\[ g(x\prime) = W \cdot x\prime. \]

We learn the explanation model weights \(W\) by minimizing the sum of proximity-weighted squared losses for all perturbed local samples:

\[ Loss = \sum_{z, z\prime} \pi_x(z) \cdot \big(f(z) - g(z\prime)\big)^2. \]

The actual learning algorithm proposed by LIME in the original paper is a LASSO. But in the actual implementation of the lime package, Ridge regression is used instead as the default learner. Despite this, the top \(K\) features for the learner are still chosen by a LASSO path.

Another discrepancy between the original paper and the actual implementation is the notion of proximity function \(\pi_x(z)\). In the actual implementation proximity is calculated in the interpretable space rather than in the original space. So essentially we should have denoted the function as \(\pi_{x\prime}(z\prime)\). Indeed, in the package source code the proximity function is defined as:2

\[ \pi_{x\prime}(z\prime) = \sqrt{ \exp \bigg( \frac{-(D(x\prime, z\prime) \times 100)^2}{\sigma^2} \bigg)}. \]

As one may realize now that the original model can be a total blackbox. We only use its predictions as labels to learn the explanation model. Be aware that here \(f(z)\) returns the predicted probability as label so the explanation model is a regressor not a classifier.

4.4 Limitations

Linearity

Notice that the local explanation model is a linear model, the explanation hence is subject to linearity. If we have any evidence suggesting heavy non-linearity around a prediction, the output of such explanation model won’t be faithful.

No Explanation for a NULL Effect

And for the local sampling, notice that we only subsample from the presence of features on the target example. So the explanation is on the presence or absence of the anything actually present in the target example. A feature that is originally not present in the example can never be part of the explanation. This could potentially miss an important null effect of a feature.

Take the same example above:

A B C H I J

It could be the case that, instead of the presence of the 6 features, the missingness of feature Z is the most important driving force for the machine learning model to make the prediction. However due to the local sampling scheme, the importance of (the null of) Z can never be estimated by the explanation model.

4.5 Hands-on Explanation Demo

4.5.1 On Text Classifiers

We use Large Movie Review Dataset to do a binary sentiment classification exercise. We will use machine learning libraries such as scikit-learn and tensorflow to quickly build a varieity of (rather complicated and hard to interpret) models and use lime to experiment explanation modeling.

import sys
print(sys.version)
3.6.2 (v3.6.2:5fd33b5, Jul  8 2017, 04:57:36) [MSC v.1900 64 bit (AMD64)]
import os
import logging
logging.getLogger("tensorflow").setLevel(logging.ERROR)
import warnings
warnings.simplefilter(action="ignore", category=UserWarning)
warnings.simplefilter(action="ignore", category=FutureWarning)

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

import tensorflow as tf
print(tf.__version__)
2.0.0
if tf.test.is_gpu_available():
  print(tf.test.gpu_device_name())
/device:GPU:0
import sklearn
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import make_pipeline
from sklearn.metrics import classification_report, roc_auc_score
from sklearn.model_selection import train_test_split
import joblib

print(sklearn.__version__)
0.22
# Create model dir to cache all models trained in the notebook.
model_dir = "models"
if not os.path.exists(model_dir):
    os.makedirs(model_dir)

# Directory to cache dataset.
home = os.path.expanduser("~")
cache_dir = os.path.join(home, ".keras")

First, we prepare the movie review dataset.3

import tensorflow_datasets as tfds

# Load the data as tf.data.Dataset.
imdb = tfds.load(name="imdb_reviews", as_supervised=True,
                 data_dir=os.path.join(home, "tensorflow_datasets"))

The dataset is a perfectly balanced dataset with 50,000 examples, half for positive and half for negative sentiment.

# Extract all texts as list since we want to use libraries other than tensorflow as well.
# And since this is a small dataset, we don't care about memory usage.
# We skip the use of a dataset iterator.
imdb_reviews_train = []
imdb_reviews_test = []
imdb_y_train = []
imdb_y_test = []
for x, y in imdb["train"].batch(128):
  imdb_reviews_train.extend(x.numpy())
  imdb_y_train.extend(y.numpy())
for x, y in imdb["test"].batch(128):
  imdb_reviews_test.extend(x.numpy())
  imdb_y_test.extend(y.numpy())

# TF works on bytes, but some other packages may only work on decoded string.
imdb_reviews_train = [b.decode("utf8") for b in imdb_reviews_train]
imdb_reviews_test = [b.decode("utf8") for b in imdb_reviews_test]
imdb_y_train = np.array(imdb_y_train)
imdb_y_test = np.array(imdb_y_test)

# Take one review.
print(imdb_reviews_train[87])
Any movie that portrays the hard-working responsible husband as the person who has to change because of bored, cheating wife is an obvious result of 8 years of the Clinton era.<br /><br />It's little wonder that this movie was written by a woman.
print(imdb_y_train[87])  # Label. 0 as negative and 1 as positive.
0

We use the data prepared by tensorflow-datasets here just to save some time. For those who want to process the data in its very original format (where one review is in one .txt file), the files can be downloaded by this piece of code:

imdb_remote_path = "https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz"
imdb_fname = os.path.basename(imdb_remote_path)
imdb_local_path = os.path.join(cache_dir, "datasets", imdb_fname)

if not os.path.exists(imdb_local_path):
  _ = tf.keras.utils.get_file(fname=imdb_fname, origin=imdb_remote_path,
                              extract=True, cache_dir=cache_dir)

Explain Random Forest

Let’s build a random forest with TF-IDF as our feature space. We will use the popular scikit-learn library for implementation.4

# We drop words that are too frequent or too rare in the training dataset.
imdb_vectorizer = TfidfVectorizer(lowercase=True, min_df=10, max_df=.9)
imdb_X_train = imdb_vectorizer.fit_transform(imdb_reviews_train)
imdb_X_test = imdb_vectorizer.transform(imdb_reviews_test)
print(len(imdb_vectorizer.vocabulary_))  # Without OOV token.
18518
imdb_rf_model_file = os.path.join(model_dir, "text_rf.joblib")

# Save/reload the model to save notebook rendering time.
if os.path.exists(imdb_rf_model_file):
  imdb_rf = joblib.load(imdb_rf_model_file)
else:
  imdb_rf = RandomForestClassifier(n_estimators=300, random_state=64, n_jobs=-2)
  _ = imdb_rf.fit(imdb_X_train, imdb_y_train)
  _ = joblib.dump(imdb_rf, imdb_rf_model_file)

imdb_rf_pred = imdb_rf.predict(imdb_X_test)
imdb_rf_yhat = imdb_rf.predict_proba(imdb_X_test)[:,1]

print(classification_report(imdb_y_test, imdb_rf_pred))
              precision    recall  f1-score   support

           0       0.84      0.86      0.85     12500
           1       0.86      0.84      0.85     12500

    accuracy                           0.85     25000
   macro avg       0.85      0.85      0.85     25000
weighted avg       0.85      0.85      0.85     25000
print(roc_auc_score(imdb_y_test, imdb_rf_yhat))
0.9274221727999999

As a baseline without extensive tuning (we didn’t tune anything indeed!), random forest seems to perform fairly well on this dataset.

As part of the algorithm’s design we are able to derive a global view of feature importance. This is based on how much each feature can reduce the impurity during all tree splittings. For example, we can plot the top 20 features:

sorted_vocab = sorted(imdb_vectorizer.vocabulary_.items(), key=lambda kv: kv[1])
sorted_vocab = [w for w, i in sorted_vocab]

imdb_rf_feat_imp = pd.Series(imdb_rf.feature_importances_, index=sorted_vocab).sort_values()
ax = imdb_rf_feat_imp.tail(20).plot(kind="barh")
plt.show()

As one can see, common adjectives describing good or bad things generally have larger impact in the model, which is totally expected. But we also see influential words such as just and minutes which are quite neutral and contain no useful information on their own. They may be jointly important in the model since a tree model allows interaction between variables. But we won’t be able to go deeper beyond the unconditional view we derived as a global feature ranking.

Interpretation of the impurity-based ranking must be very careful. For example, related features will theoretically have similar impact but only one of it will gain higher score (and suppress the other) in the ranking. Which one stands out is totally random due to the way tree splitting is performed during training.

In general it is NOT recommended to use impurity or loss-based feature ranking to interpret a tree ensemble model. Such ranking information is still useful to understand different aspects of the model, and can be used to subset feature to counter over-fitting issue, if any. But it won’t help really explain the model at the prediction-level: Why is my model making such prediction? And this is exactly why we need a explanation model in the first place.

Now move on to model explanation with LIME. We pick up one true positive and one false positive case made by our random forest model to see how the explanation model will explain each case.

from lime.lime_text import LimeTextExplainer

# We need a pipeline since LimeTextExplainer.explain_instance expects raw text input.
imdb_rf_pipe = make_pipeline(imdb_vectorizer, imdb_rf)
imdb_rf_explainer = LimeTextExplainer(class_names=["Negative", "Positive"], random_state=64)

imdb_rf_tp_idx = np.where(np.logical_and(imdb_rf_pred == 1, imdb_y_test == 1))[0]
imdb_rf_fp_idx = np.where(np.logical_and(imdb_rf_pred == 1, imdb_y_test == 0))[0]

# We take one true positive and one false positive example to demo explanation.
imdb_rf_tp_exp = imdb_rf_explainer.explain_instance(
  imdb_reviews_test[imdb_rf_tp_idx[0]], imdb_rf_pipe.predict_proba, num_features=6)
imdb_rf_fp_exp = imdb_rf_explainer.explain_instance(
  imdb_reviews_test[imdb_rf_fp_idx[0]], imdb_rf_pipe.predict_proba, num_features=6)
# For ipynb, one can simply call imdb_tp_exp.show_in_notebook(text=True) to embed the html output.

imdb_rf_tp_exp.save_to_file("/tmp/explain_text_rf_tp.html")
imdb_rf_fp_exp.save_to_file("/tmp/explain_text_rf_fp.html")
A True Positive Prediction Explained


Our RF model doesn’t seem to be very confident on this particular positive example indeed. There is no dominant single word can drive the prediction in the correct direction. The contributing words are also mostly neutral on their own. We can confirm that the result of this prediction will be very sensitive and not robust. Admittedly this review does show some mixtures of positive and negative views.

A False Positive Prediction Explained

Now let’s look at a false positive example, where our RF model wrongly labeled as a positive review.


In this example a single positive word great (wrongly) dominate the prediction toward a positive sentiment. And we realize the model didn’t response well to some negative signals, especially for the word bore.

If we examine more cases we may have more clues on how the model mis-behaves, and we can come up with a strategy accordingly to improve it. For now we’ll stop here and try experimenting with other learning algorithms hereafer.

Build LIME from Scratch

Let’s use the text data example above to build a LIME model from scratch to better understand every detail of the technique.

from scipy.sparse import csr_matrix
from sklearn.linear_model import Ridge, lars_path
from sklearn.metrics.pairwise import cosine_distances

N = 1000  # Number of local samples.
K = 10  # Number of features to used.
i = imdb_rf_tp_idx[0]  # The target example.
x = imdb_X_test[i]
local_samples_x = csr_matrix(np.ones([N, 1])) * x  # Container for perturbation.
present_tok_id = x.nonzero()[1]  # Present features.

# Generate random design matrix for the explanation model.
# This is under the interpretable (binary) space.
local_samples_z = (np.random.uniform(size=(N, len(present_tok_id))) > .5).astype(int)

# Predict local samples by the original model.
remove_ind = pd.DataFrame(zip(*np.where(local_samples_z == 0)), columns=["rid", "pos"])
for r in range(1, N):
  # We keep the first sample as the original target example.
  df = remove_ind[remove_ind.rid == r]
  if not df.empty:
    tok_ids_to_remove = present_tok_id[df.pos]
    local_samples_x[r,tok_ids_to_remove] = 0
local_samples_x.eliminate_zeros()
local_samples_y = imdb_rf.predict_proba(local_samples_x)[:,1]

# Calculate proximity weights under the interpretable space.
def pi_x(z):
  kernel_width = 25
  dist = cosine_distances(z[0].reshape(1, -1), z).ravel()
  return np.sqrt(np.exp(-((dist * 100) ** 2) / kernel_width ** 2))

weights = pi_x(local_samples_z)

# Subset top K features with LAR path.
weighted_z = ((local_samples_z - np.average(local_samples_z, axis=0, weights=weights))
  * np.sqrt(weights[:, np.newaxis]))
weighted_y = ((local_samples_y - np.average(local_samples_y, weights=weights))
  * np.sqrt(weights))
_, _, coefs = lars_path(weighted_z, weighted_y, method="lasso")

nonzero_coefs = range(weighted_z.shape[1])
for i in range(len(coefs.T) - 1, 0, -1):
    nonzero_coefs = coefs.T[i].nonzero()[0]
    if len(nonzero_coefs) <= 10:
        break

# Learn the explanation model.
explainer = Ridge(alpha=1, fit_intercept=True, random_state=64)
_ = explainer.fit(local_samples_z[:,nonzero_coefs], local_samples_y, sample_weight=weights)

# Fitness.
# This can be a score to judge how good the local approximation is.
print(explainer.score(local_samples_z[:,nonzero_coefs], local_samples_y, sample_weight=weights))
0.8105628698306793
exp = pd.DataFrame({
  "tok": np.array(sorted_vocab)[present_tok_id[nonzero_coefs]],
  "imp": explainer.coef_
})
print(exp.sort_values("imp", ascending=False))
       tok       imp
5     love  0.043018
0     also  0.030269
4      job  0.023840
9     very  0.022359
1     been -0.022332
2   better -0.023264
3     even -0.035808
7     only -0.041950
8    thing -0.062506
6  nothing -0.080989

We try a smaller local sample size in our exercise, but we can already successfully calculate very closely the feature contribution scores as in lime’s API.

Explain Neural Networks

Now let’s try a shallow neural network model with word embeddings trained from scratch. We use tensorflow.keras API to quickly build and train a neural net. We average word embeddings as the document embeddings for each review, then feed-forward a ReLU layer before the sigmoid activation for cross-entropy optimization.

As an exercise, instead of re-using the vocabulary built by TfidfVectorizer with scikit-learn, we will re-tokenize the text data with keras.preprocessing module. The inherent consistency under the Keras framework will also simplify our latter works on network layering.

from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences

# Build vocabulary. We use similar size as in our previous TfidfVectorizer.
# Since we will use zero padding, 0 cannot be used as OOV index.
# Keras tokenizer by default reserves 0 already. OOV token, if used, will be indexed at 1.
# Note that len(tokenizer.index_word) will be all vocabulary instead of `num_words`.
vocab_size = 20001  # +1 for 0 index used for padding.
oov_token = "<unk>"
tokenizer = Tokenizer(lower=True, oov_token=oov_token, num_words=vocab_size)
tokenizer.fit_on_texts(imdb_reviews_train)

# Encode text with padding to ensure fixed-length input.
seq_train = tokenizer.texts_to_sequences(imdb_reviews_train)
seq_train_padded = pad_sequences(seq_train, padding="post")
maxlen = seq_train_padded.shape[1]
seq_test = tokenizer.texts_to_sequences(imdb_reviews_test)
seq_test_padded = pad_sequences(seq_test, padding="post", maxlen=maxlen)

assert tokenizer.index_word[1] == oov_token
assert seq_train_padded.max() == vocab_size - 1

# Wrap Keras Sequential model with scikit-learn API.
# This is because LimeTextExplainer seems buggy with a native Keras model.
nn_model_file = os.path.join(model_dir, "text_clf_nn.h5")

def nn_model_fn():
  embedding_size = 64
  model = tf.keras.Sequential([
    tf.keras.layers.Embedding(
      vocab_size, embedding_size, input_length=maxlen,
      mask_zero=True, name="word_embedding"),
    tf.keras.layers.GlobalAveragePooling1D(name="doc_embedding"),
    tf.keras.layers.Dense(embedding_size / 2, activation="relu", name="relu"),
    tf.keras.layers.Dense(1, activation="sigmoid", name="sigmoid")
  ], name="nn_classifier")
  model.compile(optimizer="adam",
                loss="binary_crossentropy",
                metrics=["accuracy"])
  return model

print(nn_model_fn().summary(line_length=90))
Model: "nn_classifier"
__________________________________________________________________________________________
Layer (type)                            Output Shape                        Param #
==========================================================================================
word_embedding (Embedding)              (None, 2493, 64)                    1280064
__________________________________________________________________________________________
doc_embedding (GlobalAveragePooling1D)  (None, 64)                          0
__________________________________________________________________________________________
relu (Dense)                            (None, 32)                          2080
__________________________________________________________________________________________
sigmoid (Dense)                         (None, 1)                           33
==========================================================================================
Total params: 1,282,177
Trainable params: 1,282,177
Non-trainable params: 0
__________________________________________________________________________________________
None
imdb_nn = tf.keras.wrappers.scikit_learn.KerasClassifier(nn_model_fn)
if not os.path.exists(nn_model_file):
  metrics = imdb_nn.fit(
    x=seq_train_padded, y=imdb_y_train,
    batch_size=256, epochs=10,
    validation_data=(seq_test_padded, imdb_y_test),
    validation_steps=20,
    callbacks=[
      tf.keras.callbacks.EarlyStopping(monitor="val_loss", patience=2),
      tf.keras.callbacks.ModelCheckpoint(nn_model_file, monitor="val_loss", save_best_only=True)
    ],
    verbose=2)

# Restore the model with wrapper.
Train on 25000 samples, validate on 25000 samples
Epoch 1/10
25000/25000 - 12s - loss: 0.6431 - accuracy: 0.7548 - val_loss: 0.1110 - val_accuracy: 0.8158
Epoch 2/10
25000/25000 - 11s - loss: 0.3977 - accuracy: 0.8668 - val_loss: 0.0711 - val_accuracy: 0.8629
Epoch 3/10
25000/25000 - 11s - loss: 0.2580 - accuracy: 0.9066 - val_loss: 0.0609 - val_accuracy: 0.8801
Epoch 4/10
25000/25000 - 11s - loss: 0.2007 - accuracy: 0.9282 - val_loss: 0.0578 - val_accuracy: 0.8859
Epoch 5/10
25000/25000 - 11s - loss: 0.1622 - accuracy: 0.9443 - val_loss: 0.0577 - val_accuracy: 0.8879
Epoch 6/10
25000/25000 - 11s - loss: 0.1339 - accuracy: 0.9570 - val_loss: 0.0596 - val_accuracy: 0.8887
Epoch 7/10
25000/25000 - 11s - loss: 0.1121 - accuracy: 0.9650 - val_loss: 0.0621 - val_accuracy: 0.8869
imdb_nn.model = tf.keras.models.load_model(nn_model_file)
imdb_nn.classes_ = np.array([0, 1])
imdb_nn_yhat = imdb_nn.predict_proba(seq_test_padded)[:,1]
imdb_nn_pred = (imdb_nn_yhat > .5).astype(int)

print(classification_report(imdb_y_test, imdb_nn_pred))
              precision    recall  f1-score   support

           0       0.88      0.90      0.89     12500
           1       0.90      0.87      0.89     12500

    accuracy                           0.89     25000
   macro avg       0.89      0.89      0.89     25000
weighted avg       0.89      0.89      0.89     25000
print(roc_auc_score(imdb_y_test, imdb_nn_yhat))
0.9540818144

Based on the testing AUC score, our shallow neural network model did outperform a random forest. Let’s see how the explanation model tell us about the behavior of the neural network model.

def nn_predict_fn(text):
  # This is for sklearn wrapper only.
  seq = tokenizer.texts_to_sequences(text)
  seq = pad_sequences(seq, padding="post", maxlen=maxlen)
  return imdb_nn.predict_proba(seq)

imdb_nn_explainer = LimeTextExplainer(class_names=["Negative", "Positive"])

# Explain the same examples as in RF.
imdb_nn_tp_exp = imdb_nn_explainer.explain_instance(
  imdb_reviews_test[imdb_rf_tp_idx[0]], nn_predict_fn, num_features=6)
imdb_nn_fp_exp = imdb_nn_explainer.explain_instance(
  imdb_reviews_test[imdb_rf_fp_idx[0]], nn_predict_fn, num_features=6)

imdb_nn_tp_exp.save_to_file("/tmp/explain_text_nn_tp.html")
imdb_nn_fp_exp.save_to_file("/tmp/explain_text_nn_fp.html")


The above is the LIME explanation of the same positive example previously explained with a RF model. We realize that, though both models eventually give a positive prediction, the neural network model has a very different opinion on how the positive prediction is formulated. Instead of being confused and indecisive, the NN model is actually over-confident about this prediction! Some neutral words have disproportionate contribition to the positive, pointing out the potential direction to improve the model. For example, can a bigram tokenizer be better?

How about the second example (which is a negative review)? Our NN model also makes a mistake on this negative review, by predicting it as a positive one.


What’s different here is the reaction to the negative word bore, which is not seen in RF.

Without a explanation model, it won’t be easy for us to compare two models at this level of details.

Explain Transfer Learning

One step further, let’s use pre-trained word embeddings for the neural nets and build another explanation model. We will use GloVe (Pennington, Socher, and Manning (2014)). We use just the smaller GloVe model since our dataset is quite small.

# Download GloVe pre-trained embeddings.
# The file is about 800MB so may take some time.
glove6b_remote_path = "http://nlp.stanford.edu/data/glove.6B.zip"
glove6b_local_path = os.path.join(cache_dir, "datasets", "glove.6B.50d.txt")
glove6b_fname = os.path.basename(glove6b_remote_path)
if not os.path.exists(glove6b_local_path):
  _ = tf.keras.utils.get_file(fname=glove6b_fname, origin=glove6b_remote_path,
                              extract=True, cache_dir=cache_dir)

glove_all = pd.read_csv(glove6b_local_path, sep=" ", header=None, index_col=0, quoting=3)

In building the GloVe embeddings we need to take special care about out-of-vocabulary token AND padding index since we will be using the Keras API.

# Map vocabulary to pre-trained embeddings.
matched_toks = []
for i, w in tokenizer.index_word.items():
  if i < vocab_size:
    if w in glove_all.index:
      matched_toks.append(w)
    else:
      matched_toks.append(oov_token)

# Note that GloVe pre-trained embeddings does not include its own OOV token.
# We will use a global average embedding to represent OOV token.
print(len([t for t in matched_toks if t == oov_token]))  # How many OOVs?
861
glove_all.loc[oov_token] = glove_all.values.mean(axis=0)
glove = glove_all.loc[matched_toks].values

# Append dummy 0-index vector to support padding.
glove = np.vstack([np.zeros((1, glove.shape[1])), glove])
print(glove.shape)
(20001, 50)

Now let’s build the neural network. Most of the code will be the same as before, only the Embedding layer now we will use a constant matrix for initialization. We make the GloVe embeddings trainable so it will further adapt to our specific dataset.

tr_model_file = os.path.join(model_dir, "text_clf_tr.h5")

def tr_model_fn():
  embedding_size = glove.shape[1]
  model = tf.keras.Sequential([
    tf.keras.layers.Embedding(
      vocab_size, embedding_size, input_length=maxlen,
      embeddings_initializer=tf.keras.initializers.Constant(glove),
      trainable=True, mask_zero=True, name="glove_embedding"),
    tf.keras.layers.GlobalAveragePooling1D(name="doc_embedding"),
    tf.keras.layers.Dense(embedding_size / 2, activation="relu", name="relu"),
    tf.keras.layers.Dense(1, activation="sigmoid", name="sigmoid")
  ], name="tr_classifier")
  model.compile(optimizer="adam",
                loss="binary_crossentropy",
                metrics=["accuracy"])
  return model

print(tr_model_fn().summary(line_length=90))
Model: "tr_classifier"
__________________________________________________________________________________________
Layer (type)                            Output Shape                        Param #
==========================================================================================
glove_embedding (Embedding)             (None, 2493, 50)                    1000050
__________________________________________________________________________________________
doc_embedding (GlobalAveragePooling1D)  (None, 50)                          0
__________________________________________________________________________________________
relu (Dense)                            (None, 25)                          1275
__________________________________________________________________________________________
sigmoid (Dense)                         (None, 1)                           26
==========================================================================================
Total params: 1,001,351
Trainable params: 1,001,351
Non-trainable params: 0
__________________________________________________________________________________________
None
imdb_tr = tf.keras.wrappers.scikit_learn.KerasClassifier(tr_model_fn)
if not os.path.exists(tr_model_file):
  imdb_tr = tf.keras.wrappers.scikit_learn.KerasClassifier(tr_model_fn)
  metrics = imdb_tr.fit(
    x=seq_train_padded, y=imdb_y_train,
    batch_size=256, epochs=20,
    validation_data=(seq_test_padded, imdb_y_test),
    validation_steps=20,
    callbacks=[
      tf.keras.callbacks.EarlyStopping(monitor="val_loss", patience=2),
      tf.keras.callbacks.ModelCheckpoint(tr_model_file, monitor="val_loss", save_best_only=True)
    ],
    verbose=2)

# Restore the model with wrapper.
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
25000/25000 - 13s - loss: 0.6713 - accuracy: 0.6257 - val_loss: 0.1308 - val_accuracy: 0.7029
Epoch 2/20
25000/25000 - 12s - loss: 0.5726 - accuracy: 0.7604 - val_loss: 0.1061 - val_accuracy: 0.7818
Epoch 3/20
25000/25000 - 12s - loss: 0.4314 - accuracy: 0.8322 - val_loss: 0.0832 - val_accuracy: 0.8354
Epoch 4/20
25000/25000 - 12s - loss: 0.3322 - accuracy: 0.8724 - val_loss: 0.0716 - val_accuracy: 0.8535
Epoch 5/20
25000/25000 - 12s - loss: 0.2758 - accuracy: 0.8956 - val_loss: 0.0660 - val_accuracy: 0.8633
Epoch 6/20
25000/25000 - 12s - loss: 0.2392 - accuracy: 0.9104 - val_loss: 0.0625 - val_accuracy: 0.8734
Epoch 7/20
25000/25000 - 12s - loss: 0.2106 - accuracy: 0.9221 - val_loss: 0.0607 - val_accuracy: 0.8783
Epoch 8/20
25000/25000 - 12s - loss: 0.1882 - accuracy: 0.9318 - val_loss: 0.0598 - val_accuracy: 0.8816
Epoch 9/20
25000/25000 - 12s - loss: 0.1691 - accuracy: 0.9397 - val_loss: 0.0599 - val_accuracy: 0.8818
Epoch 10/20
25000/25000 - 11s - loss: 0.1523 - accuracy: 0.9478 - val_loss: 0.0596 - val_accuracy: 0.8859
Epoch 11/20
25000/25000 - 11s - loss: 0.1371 - accuracy: 0.9552 - val_loss: 0.0607 - val_accuracy: 0.8857
Epoch 12/20
25000/25000 - 11s - loss: 0.1240 - accuracy: 0.9605 - val_loss: 0.0631 - val_accuracy: 0.8832
imdb_tr.model = tf.keras.models.load_model(tr_model_file)
imdb_tr.classes_ = np.array([0, 1])
imdb_tr_yhat = imdb_tr.predict_proba(seq_test_padded)[:,1]
imdb_tr_pred = (imdb_tr_yhat > .5).astype(int)

print(classification_report(imdb_y_test, imdb_tr_pred))
              precision    recall  f1-score   support

           0       0.89      0.88      0.89     12500
           1       0.88      0.89      0.89     12500

    accuracy                           0.89     25000
   macro avg       0.89      0.89      0.89     25000
weighted avg       0.89      0.89      0.89     25000
print(roc_auc_score(imdb_y_test, imdb_tr_yhat))
0.9520481695999999

Our NN model with transfer learning has similar AUC score to the vanilla NN. Let’s use explanation modeling to see if there is any actual difference.

def tr_predict_fn(text):
  # This is for sklearn wrapper only.
  seq = tokenizer.texts_to_sequences(text)
  seq = pad_sequences(seq, padding="post", maxlen=maxlen)
  return imdb_tr.predict_proba(seq)

imdb_tr_explainer = LimeTextExplainer(class_names=["Negative", "Positive"])

# Explain the same examples as in RF.
imdb_tr_tp_exp = imdb_tr_explainer.explain_instance(
  imdb_reviews_test[imdb_rf_tp_idx[0]], tr_predict_fn, num_features=6)
imdb_tr_fp_exp = imdb_tr_explainer.explain_instance(
  imdb_reviews_test[imdb_rf_fp_idx[0]], tr_predict_fn, num_features=6)

imdb_tr_tp_exp.save_to_file("/tmp/explain_text_tr_tp.html")
imdb_tr_fp_exp.save_to_file("/tmp/explain_text_tr_fp.html")

For the same positive review, again the model shows over-confidence. Even the donimant words are the same.


For the negative review, interestingly, the transfer learning NN indeed makes a correct prediction of negative label. The word bore becomes the main driving force to lower down the score.


Explain Recurrent Neural Nets

As a final exercise on text classification, let’s experiment the explanation modeling with a recurrent neural network (RNN) RNN is known to be able to capture sequential dependencies better than ngram bag-of-words approach.5

rnn_model_file = os.path.join(model_dir, "text_clf_rnn.h5")

def rnn_model_fn():
  embedding_size = glove.shape[1]
  model = tf.keras.Sequential([
    tf.keras.layers.Embedding(
      vocab_size, embedding_size, input_length=maxlen,
      embeddings_initializer=tf.keras.initializers.Constant(glove),
      trainable=True, mask_zero=True, name="glove_embedding"),
    tf.keras.layers.GRU(64, dropout=.2, name="GRU"),
    tf.keras.layers.Dense(1, activation="sigmoid", name="sigmoid")
  ], name="rnn_classifier")
  model.compile(optimizer="adam",
                loss="binary_crossentropy",
                metrics=["accuracy"])
  return model

print(rnn_model_fn().summary(line_length=90))
Model: "rnn_classifier"
__________________________________________________________________________________________
Layer (type)                            Output Shape                        Param #
==========================================================================================
glove_embedding (Embedding)             (None, 2493, 50)                    1000050
__________________________________________________________________________________________
GRU (GRU)                               (None, 64)                          22272
__________________________________________________________________________________________
sigmoid (Dense)                         (None, 1)                           65
==========================================================================================
Total params: 1,022,387
Trainable params: 1,022,387
Non-trainable params: 0
__________________________________________________________________________________________
None
imdb_rnn = tf.keras.wrappers.scikit_learn.KerasClassifier(rnn_model_fn)
if not os.path.exists(rnn_model_file):
  metrics = imdb_rnn.fit(
    x=seq_train_padded, y=imdb_y_train,
    batch_size=32, epochs=10,
    validation_data=(seq_test_padded, imdb_y_test),
    validation_steps=20,
    callbacks=[
      tf.keras.callbacks.EarlyStopping(monitor="val_loss", patience=2),
      tf.keras.callbacks.ModelCheckpoint(rnn_model_file, monitor="val_loss", save_best_only=True)
    ],
    verbose=2)

# Restore the model with wrapper.
Train on 25000 samples, validate on 25000 samples
Epoch 1/10
25000/25000 - 53s - loss: 0.4712 - accuracy: 0.7609 - val_loss: 0.0067 - val_accuracy: 0.8938
Epoch 2/10
25000/25000 - 47s - loss: 0.2544 - accuracy: 0.8970 - val_loss: 0.0056 - val_accuracy: 0.9156
Epoch 3/10
25000/25000 - 47s - loss: 0.1862 - accuracy: 0.9302 - val_loss: 0.0055 - val_accuracy: 0.9125
Epoch 4/10
25000/25000 - 45s - loss: 0.1411 - accuracy: 0.9480 - val_loss: 0.0061 - val_accuracy: 0.9016
Epoch 5/10
25000/25000 - 45s - loss: 0.1075 - accuracy: 0.9612 - val_loss: 0.0066 - val_accuracy: 0.8984
imdb_rnn.model = tf.keras.models.load_model(rnn_model_file)
imdb_rnn.classes_ = np.array([0, 1])
imdb_rnn_yhat = imdb_rnn.predict_proba(seq_test_padded)[:,1]  # Interence of RNN take time.
imdb_rnn_pred = (imdb_rnn_yhat > .5).astype(int)

print(classification_report(imdb_y_test, imdb_rnn_pred))
              precision    recall  f1-score   support

           0       0.89      0.92      0.90     12500
           1       0.92      0.88      0.90     12500

    accuracy                           0.90     25000
   macro avg       0.90      0.90      0.90     25000
weighted avg       0.90      0.90      0.90     25000
print(roc_auc_score(imdb_y_test, imdb_rnn_yhat))
0.9651005920000001

RNN with pre-trained GloVe embeddings seems to work very well, even for such a small dataset. That’s see how the explanation can differ, again, for the same two examples:

def rnn_predict_fn(text):
  # This is for sklearn wrapper only.
  seq = tokenizer.texts_to_sequences(text)
  seq = pad_sequences(seq, padding="post", maxlen=maxlen)
  return imdb_rnn.predict_proba(seq)

imdb_rnn_explainer = LimeTextExplainer(class_names=["Negative", "Positive"])

# Explain the same examples as in RF.
imdb_rnn_tp_exp = imdb_rnn_explainer.explain_instance(
  imdb_reviews_test[imdb_rf_tp_idx[0]], rnn_predict_fn, num_features=6)
imdb_rnn_fp_exp = imdb_rnn_explainer.explain_instance(
  imdb_reviews_test[imdb_rf_fp_idx[0]], rnn_predict_fn, num_features=6)

imdb_rnn_tp_exp.save_to_file("/tmp/explain_text_rnn_tp.html")
imdb_rnn_fp_exp.save_to_file("/tmp/explain_text_rnn_fp.html")

The same over-confidence for all NN models on this particular positive review.


For the negative review, RNN also correctly predict the label. This may relate to they both using the pre-trained embeddings.


4.5.2 On Tabular Data Classifiers

Lots of data can be represented in tabular format. Here we will use UCI Heart Disease dataset for demo. Particularly, we use the Cleveland dataset which is commonly used in machine learning research.6

ucihd_remote_path = "https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data"
ucihd_fname = os.path.basename(ucihd_remote_path)
ucihd_local_path = os.path.join(cache_dir, "datasets", ucihd_fname)

if not os.path.exists(ucihd_local_path):
  _ = tf.keras.utils.get_file(fname=ucihd_fname, origin=ucihd_remote_path,
                              extract=False, cache_dir=cache_dir)

The dataset contains both numerical and categorical features (all encoded in numerics already, please refer to the in-line comments for documentation). It is tiny in both number of features and number of examples. But as a demo case it should serve well the purpose.

ucihd_attr = [
  "age",
  "sex",      # 0 = female 1 = male
  "cp",       # chest pain type 1: typical angina 2: atypical angina 3: non-anginal pain 4: asymptomatic
  "trestbps", # resting blood pressure (in mm Hg on admission to the hospital)
  "chol",     # serum cholestoral in mg/dl
  "fbs",      # (fasting blood sugar > 120 mg/dl) (1 = true; 0 = false)
  "restecg",  # resting electrocardiographic results 0: normal 1: having ST-T wave abnormality 2: showing probable or definite left ventricular hypertrophy by Estes' criteria
  "thalach",  # maximum heart rate achieved
  "exang",    # exercise induced angina (1 = yes; 0 = no)
  "oldpeak",  # ST depression induced by exercise relative to rest
  "slope",    # the slope of the peak exercise ST segment
  "ca",       # number of major vessels (0-3) colored by flouroscopy
  "thal",     # 3 = normal; 6 = fixed defect; 7 = reversable defect
  "label"     # diagnosis of heart disease (angiographic disease status) 0: < 50% diameter narrowing 1-4: > 50% diameter narrowing
]
ucihd = pd.read_csv(ucihd_local_path, header=None, names=ucihd_attr, na_values="?")
categorical_attr = ["sex", "cp", "fbs", "restecg", "exang", "thal"]
for col in categorical_attr:
  ucihd[col] = ucihd[col].astype("category")

# Clean label.
ucihd.loc[ucihd["label"] > 1, "label"] = 1

print(ucihd.shape)
(303, 14)
print(ucihd.groupby("label").size())  # Label distribution.
label
0    164
1    139
dtype: int64
print(ucihd.head())
    age  sex   cp  trestbps   chol  fbs restecg  thalach exang  oldpeak  slope   ca thal  label
0  63.0  1.0  1.0     145.0  233.0  1.0     2.0    150.0   0.0      2.3    3.0  0.0  6.0      0
1  67.0  1.0  4.0     160.0  286.0  0.0     2.0    108.0   1.0      1.5    2.0  3.0  3.0      1
2  67.0  1.0  4.0     120.0  229.0  0.0     2.0    129.0   1.0      2.6    2.0  2.0  7.0      1
3  37.0  1.0  3.0     130.0  250.0  0.0     0.0    187.0   0.0      3.5    3.0  0.0  3.0      0
4  41.0  0.0  2.0     130.0  204.0  0.0     2.0    172.0   0.0      1.4    1.0  0.0  3.0      0

Explain Random Forest

Again we try to explain tree ensembels.

# sklearn's implementation of RF doesn't allow missing value.
# For categorical (as string) we can leave one special category for missing,
# but for numerical we need to do some special encoding or imputation.
ucihd_2 = ucihd.copy()
ucihd_2.loc[ucihd_2["ca"].isna(), "ca"] = -1  # Encode missing numerical.

# One-hot encode all categorical features.
ucihd_2 = pd.get_dummies(ucihd_2, columns=categorical_attr, dummy_na=True)
ucihd_y = ucihd_2.pop("label")
ucihd_X_train, ucihd_X_test, ucihd_y_train, ucihd_y_test = train_test_split(
  ucihd_2, ucihd_y.values, test_size=.3, random_state=64)

ucihd_rf = RandomForestClassifier(n_estimators=100, random_state=64)
_ = ucihd_rf.fit(ucihd_X_train, ucihd_y_train)

ucihd_rf_yhat = ucihd_rf.predict_proba(ucihd_X_test)[:,1]
ucihd_rf_pred = ucihd_rf.predict(ucihd_X_test)

print(classification_report(ucihd_y_test, ucihd_rf_pred))
              precision    recall  f1-score   support

           0       0.82      0.84      0.83        50
           1       0.80      0.78      0.79        41

    accuracy                           0.81        91
   macro avg       0.81      0.81      0.81        91
weighted avg       0.81      0.81      0.81        91
print(roc_auc_score(ucihd_y_test, ucihd_rf_yhat))
0.9004878048780488

As one can see RF performs very well on this dataset.

To explain a model trained with numerical features, lime by default will discretize continous variables into quantiles for ease of interpretation. Discretization is done using statistics derived from the training dataset.

from lime.lime_tabular import LimeTabularExplainer

cat_ind = [i for i, col in enumerate(ucihd_2.columns) if "_" in col]
ucihd_rf_explainer = LimeTabularExplainer(
  ucihd_X_train.values, class_names=["Negative", "Positive"],
  feature_names=ucihd_2.columns,
  categorical_features=cat_ind)

ucihd_rf_tp_idx = np.where(np.logical_and(ucihd_rf_pred == 1, ucihd_y_test == 1))[0]
ucihd_rf_fp_idx = np.where(np.logical_and(ucihd_rf_pred == 1, ucihd_y_test == 0))[0]

# We take one true positive and one false positive for examples.
ucihd_rf_tp_exp = ucihd_rf_explainer.explain_instance(
  ucihd_X_test.iloc[ucihd_rf_tp_idx[0]], ucihd_rf.predict_proba, num_features=4)
ucihd_rf_fp_exp = ucihd_rf_explainer.explain_instance(
  ucihd_X_test.iloc[ucihd_rf_fp_idx[0]], ucihd_rf.predict_proba, num_features=4)

ucihd_rf_tp_exp.save_to_file("/tmp/explain_tab_rf_tp.html")
ucihd_rf_fp_exp.save_to_file("/tmp/explain_tab_rf_fp.html")

Following the same idea in our discussion on text classifiers, we choose two examples, one true positive and the other false positive, from the RF predictions to demonstrate explanation modeling.

A True Positive Prediction Explained


The explanation suggests several dominant features toward the positive. For categoricals each category serves as individual contribution for explanation. This is a natural consequence of one-hot encoding in our feature space.

A False Positive Prediction Explained


For the false positive case, the model is less confident. There are indeed more features driving negatively. But one strong positive contribution from the feature ca (number of major vessels colored by flouroscopy) cancel out the entire negative driving forces.

Explain Gradient Boosting Trees

Gradient boosting trees (GBT) is a powerful model family proven to work exceptionally well in many different applications. Yet due to its ensembling nature, GBT is also hard to intrepret in general.

Here we demo lightgbm’s implementation of GBT with LIME explanation.

import lightgbm as lgb

ucihd_tr = lgb.Dataset(ucihd_X_train, label=ucihd_y_train)
ucihd_te = lgb.Dataset(ucihd_X_test, label=ucihd_y_test)

ucihd_lgb_params = {
  "learning_rate": .01,
  "boosting_type": "gbdt",
  "objective": "binary",
  "metric": ["binary_logloss", "auc"],
  "num_leaves": 8,
  "max_depth": 3,
  "min_data_per_leaf": 5,
  "verbose": -1,
  "seed": 64
}

ucihd_bst = lgb.train(
  params=ucihd_lgb_params,
  num_boost_round=300, early_stopping_rounds=20,
  train_set=ucihd_tr, valid_sets=[ucihd_te],
  verbose_eval=10)
Training until validation scores don't improve for 20 rounds
[10]    valid_0's binary_logloss: 0.655886  valid_0's auc: 0.775854
[20]    valid_0's binary_logloss: 0.628993  valid_0's auc: 0.785854
[30]    valid_0's binary_logloss: 0.60576   valid_0's auc: 0.81122
[40]    valid_0's binary_logloss: 0.585625  valid_0's auc: 0.824146
[50]    valid_0's binary_logloss: 0.569745  valid_0's auc: 0.826585
[60]    valid_0's binary_logloss: 0.556003  valid_0's auc: 0.826098
[70]    valid_0's binary_logloss: 0.542218  valid_0's auc: 0.839024
[80]    valid_0's binary_logloss: 0.532141  valid_0's auc: 0.843902
[90]    valid_0's binary_logloss: 0.524471  valid_0's auc: 0.84439
[100]   valid_0's binary_logloss: 0.516474  valid_0's auc: 0.85122
[110]   valid_0's binary_logloss: 0.510493  valid_0's auc: 0.85122
[120]   valid_0's binary_logloss: 0.506372  valid_0's auc: 0.852683
[130]   valid_0's binary_logloss: 0.498944  valid_0's auc: 0.851951
[140]   valid_0's binary_logloss: 0.493064  valid_0's auc: 0.854878
[150]   valid_0's binary_logloss: 0.49013   valid_0's auc: 0.856341
[160]   valid_0's binary_logloss: 0.487886  valid_0's auc: 0.853415
Early stopping, best iteration is:
[145]   valid_0's binary_logloss: 0.491347  valid_0's auc: 0.856341
ucihd_lgb_yhat = ucihd_bst.predict(ucihd_X_test)
ucihd_lgb_pred = (ucihd_lgb_yhat > .5).astype(int)

print(classification_report(ucihd_y_test, ucihd_lgb_pred))
              precision    recall  f1-score   support

           0       0.80      0.78      0.79        50
           1       0.74      0.76      0.75        41

    accuracy                           0.77        91
   macro avg       0.77      0.77      0.77        91
weighted avg       0.77      0.77      0.77        91
print(roc_auc_score(ucihd_y_test, ucihd_lgb_yhat))
0.8563414634146341

In this particular (rather small) dataset RF indeed outperforms GBT. As a matter of fact, based on existing benchmark a simple logistic regression may have a even higher score for this problem. Nevertheless, let’s move on to our explanation model with LIME:

def ucihd_lgb_predict_fn(x):
  # We need to output 2 columns for binary prob prediction.
  p = ucihd_bst.predict(x).reshape(-1, 1)
  return np.hstack((1 - p, p))

ucihd_lgb_explainer = LimeTabularExplainer(
  ucihd_X_train.values, class_names=["Negative", "Positive"],
  feature_names=ucihd_2.columns,
  categorical_features=cat_ind)

# We take the same examples previously explained in our RF explanation model.
ucihd_lgb_tp_exp = ucihd_lgb_explainer.explain_instance(
  ucihd_X_test.iloc[ucihd_rf_tp_idx[0]], ucihd_lgb_predict_fn, num_features=4)
ucihd_lgb_fp_exp = ucihd_lgb_explainer.explain_instance(
  ucihd_X_test.iloc[ucihd_rf_fp_idx[0]], ucihd_lgb_predict_fn, num_features=4)

ucihd_lgb_tp_exp.save_to_file("/tmp/explain_tab_lgb_tp.html")
ucihd_lgb_fp_exp.save_to_file("/tmp/explain_tab_lgb_fp.html")

The behavior of GBT looks similar to that of RF in terms of these two examples.


In both case, the variable ca has a dominant impact on the final decision. The two modesl also share the same confusion against the negative example.


Optimized Categorical Encoding in lightgbm

This section is a digression on lightgbm usage.

Since lime’s API requires us to prepare our dataset in one-hot encoding representation, our lightgbm code use the same data pipeline as in scikit-learn random forest. But that is actually not optimized for lightgbm. The following code chunk showcases the best practice of encoding categoricals in lightgbm: We don’t encode them at all!

# We leave both missings and categoricals as-is in the dataset.
ucihd_train, ucihd_test = train_test_split(ucihd, test_size=.3, random_state=64)
ucihd_tr = lgb.Dataset(
  ucihd_train.drop("label", axis=1), label=ucihd_train["label"],
  categorical_feature=categorical_attr,
  free_raw_data=False)
ucihd_te = lgb.Dataset(
  ucihd_test.drop("label", axis=1), label=ucihd_test["label"],
  categorical_feature=categorical_attr,
  free_raw_data=False)

ucihd_bst_2 = lgb.train(
  params=ucihd_lgb_params,
  num_boost_round=300, early_stopping_rounds=20,
  train_set=ucihd_tr, valid_sets=[ucihd_te],
  verbose_eval=-1)
Training until validation scores don't improve for 20 rounds
Early stopping, best iteration is:
[94]    valid_0's binary_logloss: 0.519726  valid_0's auc: 0.852683
ucihd_lgb_yhat = ucihd_bst_2.predict(ucihd_test.drop("label", axis=1))
ucihd_lgb_pred = (ucihd_lgb_yhat > .5).astype(int)

print(roc_auc_score(ucihd_test["label"], ucihd_lgb_yhat))
0.8526829268292683

To summarize, There are two very special properties about lightgbm algorithm. lightgbm treats missings natively as a special tree split point. This allows us to keep the original missing as is and in many cases can result in better accuracy than imputation.7

In addition, lightgbm encodes categorical variables internally in a more efficient way. So we don’t even need to do one-hot encoding on our own. Of course in this tiny dataset we won’t see any noticable difference. But for large applications the performance impact can be huge. Whatever, by skipping one-hot encoding pipeline our code can be much neater as well.

4.5.3 On Image Classifiers

TODO: Use a pre-trained model?

5 Shapley Value

Shapley value is a game theory term. In a cooperative game with \(n\) players (denoted as set \(N\) with \(\vert N\vert = n\)), Shapley value of a player is its contribution to the total payoff of the game, after taking into account all possible coalition among players.

5.1 A Cooperative Game

When each player in a cooperative game may contribute differently to the payoff, Shapley value is a way to determine how important each player is to the final outcome.

We define a function \(\nu(S)\) where \(S\) is a set of \(m\) players (\(\vert S\vert = m\)), the value of \(\nu\) is the expected payoff from all members in \(S\) as a coalition. Shapley value suggests that the contribution of an individual member \(j\) is calculated as the following formula:

\[ \varphi_j(\nu) = \sum _{S\subseteq N\setminus \{j\}} \gamma(S, N) \cdot \bigg[ \nu(S_{j+}) - \nu(S_{j-}) \bigg], \]

where

\[ \gamma(S, N) = \frac {m!(n - m - 1)!}{n!} \]

is the permutation proportional weight, and \(N\setminus \{j\}\) is the entire player set except player \(j\), \(S_{j+}\) denotes a coalition with player \(j\), \(S_{j-}\) denotes a coalition without player \(j\).

So essentially Shapley value of player \(j\) is the weighted average payoff difference between all possible team work compositions (coalitions) with and without \(j\).

5.2 ML Models As Cooperative Games

Lipovetsky and Conklin (2001) postulate a machine learning model inference task as such a cooperative game. Each feature value is now a player, the payoff is the difference between the final predicted value and the average prediction value. In this way, Shapley value of a feature can well represent its importance in the contribution to the prediction. Based on Shapely values of all features, we are able to explain a blackbox prediction.8

Now the real question is how can we implement the characteristic function \(\nu(S)\) in order to calculate the payoff difference between any two feature coalitions? There are many different implementations on this. Here we briefly discuss some of them.

5.2.1 Obtain \(\nu(S)\) with Model Re-Training

\(\nu\) in this case is exactly the model prediction function. One obvious way of calculating the term \(\nu(S_{j+}) - \nu(S_{j-})\) for a given feature coalition is hence to train two separate models, one with only the coalition as feature sets (\(S_{j-}\)) and the other with the coalition plus the feature \(j\) (\(S_{j+}\)). Though using such method we can obtain the exact Shapley value, obviously this will be infeasible for large applications.

5.2.2 Obtain \(\nu(S)\) with Replacement Sampling

To bypass the need for re-training with an approximation solution, another way to obtain \(\nu(S)\) is to marginalize the effect of all the features not present in the coalition set \(S\). This is done by predict the same target instance, but with its non-coalition feature values replaced with random draws from the data.

This is better illustrated with an example. Let’s take the first row of the UCI Heart Disease data we explored just before:

print(ucihd.iloc[0])
age          63
sex           1
cp            1
trestbps    145
chol        233
fbs           1
restecg       2
thalach     150
exang         0
oldpeak     2.3
slope         3
ca            0
thal          6
label         0
Name: 0, dtype: object

To make the example neat, assuming we are only looking at a model trained on the following four attributes: age, sex, cp, ca. Our goal now is to explain that model prediction given these 4 feature values. We illustrate the idea using the following plot (assuming age is the feature to calculate Shapley value):

Each row in the plot represents a possible coalition case. For example: Row 1 indicates there is no coalition at all. Row 7 indicates a coalition of the rest 3 features. In each case, features not included in the coalition are randomized by replacing them with a random draw from the dataset. For each coalition case if we generate enough such (artificial) instances and average the result, we marginalize their effects on the model, as if they are not included in the model in the first place.9

For the target feature to calculate Shapley value, we also use the same randomization technique to indicate whether it is in or out of the coalition. Now \(S_{j+}\) can be obtained by averaging model predictions over all randomized samples with feature j included in the coalition and all the other features replaced by random draws. And similarly for \(S_{j-}\) with only one tweak: feature \(j\) now is also randomized. For each possible coalition (corresponding to each row in the plot) we need to do this marginalization computation. After traverse through all possible coalitions, we take the weighted average of all prediction differences to arrive at the estimated Shapley value, with the weighting function \(\gamma(S, N)\).

Even though this approach eliminates the need to re-train a pair of models for every coalition, number of possible coalitions still grow exponentially with total number of features. As a result, in reality, it is still not feasible for any large application.

5.2.3 Obtain \(\nu(S)\) with Monte Carlo

To further reduce the computing time, another approach is to use Monte Carlo samples to approximate combinations of coalitions. So essentially the task will be simplified to solve:

\[ \hat\varphi_j(\nu) = \frac{1}{R} \sum _{r = 1}^R \bigg[ \nu(S_{j+}^r) - \nu(S_{j-}^r) \bigg], \]

where \(R\) is number of Monte Carlo samples, \(S_{j+}^r\) is a feature set with a random number of features whose value being replaced with random draws from the data, but fixing feature \(j\). \(S_{j-}^r\) is almost the same except for also randomizing feature \(j\).

This approximation makes Shapley value approach feasible now even for large applications.

5.3 From Shapley to SHAP

In theory a Shapley value considers all possible feature interactions and is global for the underlying machine learning model. This is very different from LIME where the explanation is only local to the original model. Consequently Shapley explanations have better properties than LIME. We can interpret individual prediction, or we can calculate Shapley values for all examples and aggregate the effect to profile global feature importance. Again the original model can be any blackbox algorithm. All we need is the access to its prediction interface AND also the original training dataset in order to do the sampling approximation.

We will skip the hands-on exercise on Shapley value approach, jumping directly to a more general and also computationally efficient approach: SHAP But the discussion here will definitely help us understand SHAP since they are closely related to each other.

6 SHAP

Lundberg and Lee (2017) propose SHAP (SHapley Additive exPlanations), yet another additive feature attribution method for model explainability. As its name suggests, SHAP is based up top of Shapley value. It is indeed a more general approach unifying both LIME and Shapley value (and more). Of course SHAP is also model-agnostic. In theory it can be applied to any machine learning model, but shap comes with a customized fast implementation particularly for gradient boosting trees (GBT). It supports APIs of well-known open source GBT libraries such as xgboost, lightgbm, and catboost.

shap also comes with more visualization methods for feature investigation, especially for feature interaction exploration. Before we proceed to the hands-on section, let’s briefly explore the methodology first.

6.1 Estimate Shapley Value with a Linear Model

Remember that the additive feature attribution method for an explanation model \(g(\cdot)\) has the follwoing form:

\[ g(z\prime) = \phi_0 + \sum_{j = 1}^n \phi_j z_j\prime. \]

LIME estimates the contribution factor \(\phi_j\) using a local regularized regression model, with a weighting function measuring proximity between the sampled binarized features \(z\prime\) and the target binarized feature \(x\prime\). SHAP, instead, postulates \(\phi_j\) as the Shapley value of feature \(j\). It turns out that if we correctly setup the weighting function in a linear model, we can obtain the Shapley value using a weighted regression with randomized coalition sampling (just as what we discussed here).

The weighting function proven to be able to recover Shapley value is:

\[ \pi_{x\prime}(z\prime) = \frac{m - 1} {\binom{m}{\vert\ z\prime\vert}\vert z\prime\vert(m - \vert z\prime\vert)}, \]

where \(m\) is the maximum coalition size possible for the given example \(z\), and \(\vert z\prime\vert\) is the size of current coalition.

Now we optimize the loss:

\[ Loss = \sum_{z, z\prime} \pi_{x\prime}(z\prime) \cdot \big(f(z) - g(z\prime)\big)^2. \]

(This objective function is in general the same as in LIME.)

In shap, the KernelExplainer implements the weighted linear model to estimate Shapley value, by the following procedure:

  1. Construct a number of random coalition vectors to subset the features of target example
  2. Transoform each sample back to the original feature space
    1. For each coalition, do random sample replacement to impute features that are randomly marked as absent
    2. Feed the transformed features into the original blackbox model to obtain predictions as labels
  1. Fit all coalition vectors with a weighted linear regression

Step 2 is to marginalize out the impact of absent features, which we’ve discussed already in the Shapley value section.

6.1.1 SHAP Kernel v.s. LIME

This approach closely connects LIME with Shapley value approach. We summarize the major difference between SHAP and LIME.

Weighting Function

In LIME the choice of the weighting function \(\pi_x\prime\) is heuristic, while in SHAP it is based on a theory to recover the Shapley value.

Regularization

Based on the theory, SHAP uses a linear regression without regularization in order to faithfully recover Shapley value. In LIME instead a LASSO path is used to prefer a sparse solution.

6.1.2 The Intuition

We (purposely) skip the discussion on proving the above weighted linear model can recover Shapley value. For readers who are interested please refer to the supplementary material to the original paper. Here instead we discuss the intuition behind the scene.

If we look at the definition of Shapley value:

\[ \varphi_j(\nu) = \sum _{S\subseteq N\setminus \{j\}} \frac {m!(n - m - 1)!}{n!} \bigg[ \nu(S_{j+}) - \nu(S_{j-}) \bigg], \]

we realize that we are measuring an expected value of difference. To estimate such difference of a binary variable we know that we can simply include a dummy variable in our design matrix for a regression problem. Now the only task left is to specify a correct weighting scheme such that the expectation will take into consideration how feature coalition can be formed. SHAP is novel since it derives the correct weighting scheme such that the regression coefficients are exactly Shapley value.

6.2 Estimate Shapley Value with Tree Ensembles

A limitation on the above linear regression approach is the assumption of feature independence. It is a natural consequence when we do coalition sampling by marginalizing out the absent features in order to construct regression samples to estimate the Shapley value.

Lundberg, Erion, and Lee (2018) extend the work in shap, introduce TreeExplainer to release such constraint with a model-specific algorithm to calculate Shapley value. This can be done due to the nature of tree algorithm. The marginalization phase can take into account feature dependency by examine the tree structure and only average over terminal nodes conditional on any given node, based on the coalition set.

In TreeExplainer the exact Shapley value is directly calculated using tree nodes instead of a linear model. One added-on benefit is to extend the Shapley value to Shapley interaction value so essentially a feature attribution vector will be generalized to a feature attribution matrix, with the interaction with all other features.

Let’s skip the details and move on to the actual hands-on examples!

6.3 Hands-on Explanation Demo

6.3.1 On Text Classifiers

Explain Random Forest

shap.TreeExplainer performance is bad for scikit-learn’s RandomForestClassifier.10 To fully explore the capability we will skip the RF and move on to a GBT model.

Explain Gradient Boosting Trees

In the previous section we didn’t train a GBT for the text classification problem. So let’s quickly build one such model first (with the same TF-IDF vectorization as we did for the RF model).

# lightgbm does not allow utf-8 encoded feature names.
# Since important tokens are most likely ascii-compatible for our dataset,
# we simply strip non-ascii as a workaround for this exercise.
def remove_non_ascii(s):
  return "".join([i if ord(i) < 128 else "_" for i in s])

sorted_vocab_ascii = [remove_non_ascii(v) for v in sorted_vocab]

imdb_X_tr = lgb.Dataset(imdb_X_train, label=imdb_y_train, feature_name=sorted_vocab_ascii)
imdb_X_te = lgb.Dataset(imdb_X_test, label=imdb_y_test, feature_name=sorted_vocab_ascii)

imdb_lgb_params = {
  "learning_rate": .05,
  "boosting_type": "gbdt",
  "objective": "binary",
  "metric": ["binary_logloss", "auc"],
  "num_leaves": 16,
  "max_depth": 4,
  "min_data_per_leaf": 20,
  "verbose": -1
}

imdb_lgb_model_file = os.path.join(model_dir, "text_clf_lgb.txt")

# Save/reload model to save notebook rendering time.
if os.path.exists(imdb_lgb_model_file):
  # Parameters are not loaded back? (Which cause the subsequent call to shap_values fail.)
  # https://github.com/microsoft/LightGBM/issues/2613
  # As a workaround we pass the same parameters to re-construct the model.
  imdb_bst = lgb.Booster(model_file=imdb_lgb_model_file, params=imdb_lgb_params)
else:
  imdb_bst = lgb.train(
    params=imdb_lgb_params,
    num_boost_round=1000, early_stopping_rounds=20,
    train_set=imdb_X_tr, valid_sets=[imdb_X_te],
    verbose_eval=100)
  _ = imdb_bst.save_model(imdb_lgb_model_file)
Training until validation scores don't improve for 20 rounds
[100]   valid_0's binary_logloss: 0.479194  valid_0's auc: 0.88184
[200]   valid_0's binary_logloss: 0.424974  valid_0's auc: 0.906732
[300]   valid_0's binary_logloss: 0.394577  valid_0's auc: 0.918715
[400]   valid_0's binary_logloss: 0.374584  valid_0's auc: 0.925959
[500]   valid_0's binary_logloss: 0.359882  valid_0's auc: 0.930936
[600]   valid_0's binary_logloss: 0.348809  valid_0's auc: 0.934481
[700]   valid_0's binary_logloss: 0.340231  valid_0's auc: 0.937016
[800]   valid_0's binary_logloss: 0.333412  valid_0's auc: 0.938953
[900]   valid_0's binary_logloss: 0.327711  valid_0's auc: 0.94053
[1000]  valid_0's binary_logloss: 0.323358  valid_0's auc: 0.94162
Did not meet early stopping. Best iteration is:
[1000]  valid_0's binary_logloss: 0.323358  valid_0's auc: 0.94162
imdb_lgb_yhat = imdb_bst.predict(imdb_X_test)
imdb_lgb_pred = (imdb_lgb_yhat > .5).astype(int)

print(classification_report(imdb_y_test, imdb_lgb_pred))
              precision    recall  f1-score   support

           0       0.88      0.85      0.86     12500
           1       0.85      0.88      0.87     12500

    accuracy                           0.86     25000
   macro avg       0.86      0.86      0.86     25000
weighted avg       0.86      0.86      0.86     25000
print(roc_auc_score(imdb_y_test, imdb_lgb_yhat))
0.9416203136

Based on the testing AUC score we find out that GBT performs comparably to neural network models.

Just like RF we will have access to the overall feature importance with a GBT model:11

ax = lgb.plot_importance(imdb_bst, max_num_features=20)
plt.show()

The global feature ranking reveals some highly ranked features to be meaningless on its own. Especially the word it. But as discussed earlier we shouldn’t over-interpret the ranks without a proper explanation modeling.

Since shap.TreeExplainer is customized for GBT for speed, we can feed in all testing examples to calculate all shap values at once.

import shap

# Sparse matrix is supported by shap for lightgbm models.
imdb_lgb_explainer = shap.TreeExplainer(imdb_bst)
imdb_lgb_shap_values = imdb_lgb_explainer.shap_values(imdb_X_test)

def imdb_lgb_shap_plot(test_id, matplotlib=True):
  shap_plt = shap.force_plot(
    imdb_lgb_explainer.expected_value[1],
    imdb_lgb_shap_values[1][test_id,:],
    imdb_X_test[test_id,:].toarray(),  # We still need a dense matrix here.
    feature_names=sorted_vocab,
    matplotlib=matplotlib
  )
  return shap_plt
Global Importance

One advantage of shap on GBT models is the capability of traverse through all the testing examples due to its efficiency. So we can based on all the resulting shap values to derive a global feature importance judged by their average shap values (contributions). Note that this is different from the loss/impurity or split time-based feature ranking derived from RF/GBT during training. It is an aggregation from all local prediction explanations (contributions) during testing data inference.

shap.summary_plot(imdb_lgb_shap_values, imdb_X_test, feature_names=sorted_vocab,
                  plot_type="bar", max_display=20, show=False, plot_size=.25)
plt.show()

As we can see, the ranking based on shap values for testing set will be in general different from the ranking based on training split. And it is more interpretable: Features with higher rank literally have averagely higher impact on the testing dataset. Also the ranking can be conditioned on labels.

Local Explanation

The most important application of shap still lies on instance-level explanation. We stick to the previous two reviews. For the review that RF correctly label positive, we have the shap explanation with the following visualization:

imdb_lgb_shap_plot(imdb_rf_tp_idx[0])

Note that by default shap for lightgbm shows log-odds rather than probability in the plot. So a positive value indicates a positive prediction, otherwise negative.

To verify this:

def to_log_odds(p):
  return np.log(p / (1 - p))

def to_p(log_odds):
  return np.exp(log_odds)/(1 + np.exp(log_odds))

# Take the first true positive to examine:
p = imdb_bst.predict(imdb_X_test[imdb_rf_tp_idx[0],:].toarray())
print(p)
[0.71683258]
print(to_log_odds(p))  # This is the reported number on the default shap plot.
[0.92880398]

For any given prediction, the shap values of all features should sum up to the difference between the predicted log-odds and the expected log-odds. To verify this on the specific positive example:

expected_log_odds = imdb_lgb_explainer.expected_value[1]
predicted_log_odds = to_log_odds(p)

print(predicted_log_odds - expected_log_odds)  # The difference.
[1.09763611]
shap_v = pd.DataFrame({
  "token": sorted_vocab,
  "shap_value": imdb_lgb_shap_values[1][imdb_rf_tp_idx[0],:],
  "tfidf": np.squeeze(imdb_X_test[imdb_rf_tp_idx[0]].toarray())
})
shap_v = shap_v.sort_values("shap_value", ascending=False)
print(shap_v)  # Shap values of all features for the example.
            token  shap_value     tfidf
8464   incredible    0.469118  0.138549
9893         love    0.358313  0.076663
4420   definitely    0.267002  0.109114
1420          bad    0.200221  0.000000
728          also    0.188378  0.066295
...           ...         ...       ...
8908           it   -0.135065  0.031411
1773       better   -0.136536  0.075703
7329        great   -0.173293  0.000000
14932       silly   -0.259718  0.125069
11305     nothing   -0.725885  0.084064

[18518 rows x 3 columns]
print(shap_v.shap_value.sum())  # The sum of shap values.
1.0976361111226303

From the entire shap values we can know for example that the absence of great contributes negatively, and the presence of love contributes positively, to the final prediction.

imdb_lgb_shap_plot(imdb_rf_fp_idx[0])

For the false positive case, similar to RF, the word great play a big role in shaping the GBT prediction toward positive.

Explain Neural Nets with Word Embeddings

As of 2019-12-14 shap.DeepExplainer does not yet support TF 2.0.12 And shap.GradientExplainer is not well documented yet for TF 2.0. So we will use the shap.KernelExplainer which is a implementation-agnostic explainer in shap. The compromise is that it will run very slow for each prediction.

imdb_exp_ind = np.array([imdb_rf_tp_idx[0], imdb_rf_fp_idx[0]])
# KernelExplainer.
def mm(X):
  return imdb_tr.predict_proba(X)[:,1]

imdb_nn_shap_explainer = shap.KernelExplainer(mm, seq_train_padded[:100])
# This is VERY slow...
#imdb_nn_kernel_shap_values = imdb_nn_shap_explainer.shap_values(seq_test_padded[imdb_exp_ind])
# TODO:
# Contribution is attributed to original sequence input.
# In order to make explanation readable,
# we need to map each position to original word id then to word.
# TODO: Makje sure everything works here.

# shap does not support keras model in scikit-learn wrapper.
# Let's re-build the model and retain its Sequental class.
dl_model = model_fn()
metrics = dl_model.fit(
  x=seq_train_padded, y=imdb_y_train,
  batch_size=256, epochs=20,
  validation_data=(seq_test_padded, imdb_y_test),
  validation_steps=20,
  callbacks=[
    tf.keras.callbacks.EarlyStopping(monitor="val_loss", patience=2),
    tf.keras.callbacks.ModelCheckpoint(tr_model_file, monitor="val_loss", save_best_only=True)
  ],
  verbose=0)

# DeepExplainer.
dl_shap_explainer = shap.DeepExplainer(dl_model, seq_train_padded)  # Wont' work.

# GradientExplainer.
imdb_nn_shap_explainer = shap.GradientExplainer(dl_model, seq_train_padded[:100])

imdb_nn_shap_explainer = shap.GradientExplainer(
  (imdb_tr.layers[0].input, imdb_tr.layers[-1].output),  # Not working for TF 2.0.
  seq_train_padded[:100])
imdb_nn_shap_explainer.shap_values(seq_test_padded[:3])  # Error here.

6.3.2 On Tabular Data Classifiers

We do the same exercise on the tabular dataset previously explained by lime.

Explain Random Forest

ucihd_rf_explainer = shap.TreeExplainer(ucihd_rf)
ucihd_rf_shap_values = ucihd_rf_explainer.shap_values(ucihd_X_test)

def ucihd_rf_shap_plot(test_id, matplotlib=True):
  shap_plt = shap.force_plot(
    ucihd_rf_explainer.expected_value[1],
    ucihd_rf_shap_values[1][test_id,:],
    ucihd_X_test.iloc[[test_id]],
    matplotlib=matplotlib
  )
  return shap_plt
Global Feature Importance

From the global ranking we can confirm that variable ca is definitely an influential feature.

Shap value feature ranking
shap.summary_plot(ucihd_rf_shap_values, ucihd_X_test,
                  plot_type="bar", max_display=10, show=False, plot_size=.25)
plt.gcf().subplots_adjust(bottom=.25, left=.25)
plt.show()

Split-time-based feature ranking
ucihd_rf_feat_imp = pd.Series(ucihd_rf.feature_importances_, index=ucihd_X_train.columns).sort_values()
ax = ucihd_rf_feat_imp.tail(10).plot(kind="barh")
plt.show()

Feature Interaction

We can plot partial dependency based on shap values of two features over the entire testing dataset. For example, by knowing that ca is important, we’d like to further know how age can impact the contribution of ca across different examples.

shap.dependence_plot("age", ucihd_rf_shap_values[1], ucihd_X_test, interaction_index="ca", show=False)
plt.gcf().subplots_adjust(left=.25)
plt.show()

The result suggests two things:

  1. The model will predict higher risk for older people
  2. ca has less impact for yonger people

Both can be examined by domain-experts to see if the model is learning the correct pattern that we expected or at least that we can reason.

Local Explanation

Note that for scikit-learn RF model by default shap reports probability instead of log-odds. Such behavior difference results from the optimization customized for GBT model family.

# The true positive case in RF.
ucihd_rf_shap_plot(ucihd_rf_tp_idx[0])

# The false positive case in RF.
ucihd_rf_shap_plot(ucihd_rf_fp_idx[0])

Explain Gradient Boosting Trees

For GBT we feed the model that is optimized, where categoricals are encoded internally without explicit one-hot encoding.

ucihd_lgb_explainer = shap.TreeExplainer(ucihd_bst_2)
ucihd_lgb_shap_values = ucihd_lgb_explainer.shap_values(ucihd_test.drop("label", axis=1))

def ucihd_lgb_shap_plot(test_id, matplotlib=True):
  shap_plt = shap.force_plot(
    ucihd_lgb_explainer.expected_value[1],
    ucihd_lgb_shap_values[1][test_id,:],
    ucihd_test.iloc[[test_id]].drop("label", axis=1),
    matplotlib=matplotlib
  )
  return shap_plt
Global Feature Importance
Split-time-based feature ranking
ax = lgb.plot_importance(ucihd_bst_2, max_num_features=10)
plt.show()

Shap value feature ranking
shap.summary_plot(ucihd_lgb_shap_values, ucihd_test.drop("label", axis=1),
                  plot_type="bar", max_display=10, show=False, plot_size=.25)
plt.gcf().subplots_adjust(bottom=.25)
plt.show()

Feature Interaction
shap.dependence_plot("age", ucihd_lgb_shap_values[1],
                     ucihd_test.drop("label", axis=1), interaction_index="ca", show=False)
plt.gcf().subplots_adjust(left=.25)
Local Explanation
ucihd_lgb_shap_plot(ucihd_rf_tp_idx[0])

ucihd_lgb_shap_plot(ucihd_rf_fp_idx[0])

Impact of One-Hot Encoding On Explanation

As one may now realize, by explicitly one-hot-encode the categorical features we essentially split them into different features in their interpretable representation. This can be either good or bad, depending on the actual use case. From this particular aspect libary such as lightgbm provides the flexibility to allow us choose whether to do the one-hot encoding or not. So the way we want to construct the explanation model may well affect our implementation of the original model.

6.3.3 On Image Classifiers

TODO: Use a pre-trained model?

7 Explainable Boosting Machine

Nori et al. (2019) publish the open source package interpret for a fast implementation of Generalized Additive Models with Pairwise Interactions, or GA2M (Lou et al. (2013)). As of 2019-12-14, interpret is still in its alpha release with limited documentation. The library contains two groups of modeling frameworks:

  • glassbox: explanable machine learning models
  • blackbox: machine learning explanation models (such as LIME and SHAP)

We’ve already covered the mainstream approach in the second group, i.e., models that approximate (locally) the original model (supposed to be a blackbox) for better explainability. The more interesting part of interpret is to bring about another type of model that is readily interpretable from its very origin, and yet still competitively accurate: the Explainable Boosting Machine, or EBM.

EBM is an additive model of the form:

\[ g(E(y)) = \beta_0 + \sum f_j (x_j) + \sum f_{ij}(x_i, x_j), \]

where \(g(\cdot)\) is a link function (sigmoid for binary classification, for an example), \(f_j\) is the feature function for the \(j\)-th feature, learned by a gradient boosting algorithm with only that feature at a time and in a round-robin fashion for all features. \(f_{ij}\) is a pairwise interaction feature function to further boost the accuracy of the model while remain interpretability.

The model is interpretable since the contribution of any individual feature can be directly quantified by their corresponding feature function \(f_j\). Such explanation can extend up to pairwise interaction if pairwise feature functions are also estimated.

7.1 Feature Shape Functions

The individial feature functions are also referred to as shape functions in GAM literature. The name probably comes after the fact that upon finishing learning the function, we can plot its output value \(f_j(x_j)\) against its input value \(x_j\), effectively visaulize the shape of possible contributions of that feature along its values over a dataset.

Note that although GAM is linear due to its additivity, each individual shape function can be (and mostly is) non-linear. Lou, Caruana, and Gehrke (2012) has done a comprehensive experiment on GAM (without variable interaction) over several datasets. They found that bagged trees as shape functions and gradient boosting as GAM learner has the best accuracy over several other choices.

To briefly summarize the gradient boosting training loop, here is the pseudo code:

set total iteration = M
set total feature = N
initialize all f_j with 0
for m in 1 to M:
  for j in 1 to N:
    calculate residuals with full GAM model
    learn f_j against residuals and update the full GAM model

7.2 FAST Pairwise Interaction Detection

It will be infeasible to include all pairwise interaction since the number of possible pairs grows quadratically with total number of features. GA2M proposes an efficient way to rank potentially significant interaction pairs to largely reduce number of feature functions to learn. The pair ranking or detecting algorithm is called FAST in the original paper.

Given the current best model, potential interactions are detected on model residual. And the residual sum of squares (RSS) is used as the criteria whether to include additional interaction. If RSS does not reduce enough, it is suggesting the additional interaction is doing no benefit to the current best model.

In a bit more details, FAST contains two stages:

  1. [STAGE 1] Build the best GAM without interactions, i.e., \(g(E(y)) = \beta_0 + \sum f_j (x_j)\)
  2. [STAGE 2] Fix \(f_j\) for all \(j\), iteratively build interaction functions \(f_{ij}\) based on RSS reduction

In stage one the process of learning one-dimensional feature functions is also called feature shaping. In stage two where we extend GAM to GA2M, \(f_{ij}\) is a bivariate tree model with efficient implementation based on cumulative histograms of the two involving features. Additionally, continuos features are further discretized into bins (say, 256 bins) of equi-frequency to speed up the tree split training.13

7.3 Global Feature Ranking

GA2M rank each feature importance (either one-dimensional or interaction) by the standard deviation of the function values: \(\sqrt{E(f_j^2)}\) Note that in a reduced form where feature function itself is linear: \(f_j(x_j) = w_jx_j\), according to the formula the weight directly corresponds to the importance value: \(\sqrt{E(f_j^2)} = w_j\).

In plain words, the importance score measures how volatile the contribution of a given feature is over a given training set. A feature with higher volatility essentially play a bigger role in shaping the final model decision. Hence it is (globally) more important.

7.4 Hands-on Explanation Demo

7.4.1 On Text / Image Data

EBM is not efficient for text dataset. Due to the algorithm’s design it will run too long for bag-of-words model since there are too many feature functions to estimate. If we fit a EBM with the movie review dataset (definitely not a big one), we will encounter OOM (out-of-memory) issue even without interaction terms. As a result, we will skip the discussion of EBM on a text classifier. (The same restriction applies to image dataset.)

7.4.2 On Tabular Data

ExplainableBoostingClassifier has a scikit-learn fashion API and hence is straightforward to use.

from interpret.glassbox import ExplainableBoostingClassifier

ucihd_ebm = ExplainableBoostingClassifier(
  n_estimators=16, feature_names=ucihd_2.columns, n_jobs=1)
_ = ucihd_ebm.fit(ucihd_X_train, ucihd_y_train)
ucihd_ebm_yhat = ucihd_ebm.predict_proba(ucihd_X_test)[:,1]
ucihd_ebm_pred = (ucihd_ebm_yhat > .5).astype(int)

print(classification_report(ucihd_y_test, ucihd_ebm_pred))
print(roc_auc_score(ucihd_y_test, ucihd_ebm_yhat))

The model performs very well on the heart disease dataset, outperforming both RF and GBT.14

Global Explanation

interpret comes with a rich set of visualization tools (with plotly as its backend). Model explanation is divided into two groups: global and local.

For global explanation, we have access to both global feature importance and a per-feature feature contribution stats.

ucihd_ebm_global = ucihd_ebm.explain_global()
# All feature info:
print(ucihd_ebm_global.selector)
           Name         Type  # Unique  % Non-zero
0           age   continuous        40       1.000
1      trestbps   continuous        45       1.000
2          chol   continuous       129       1.000
3       thalach   continuous        83       1.000
4       oldpeak   continuous        39       0.679
5         slope   continuous         3       1.000
6            ca   continuous         5       0.387
7       sex_0.0  categorical         2       0.335
8       sex_1.0  categorical         2       0.665
9       sex_nan  categorical         1       0.000
10       cp_1.0  categorical         2       0.080
11       cp_2.0  categorical         2       0.170
12       cp_3.0  categorical         2       0.292
13       cp_4.0  categorical         2       0.458
14       cp_nan  categorical         1       0.000
15      fbs_0.0  categorical         2       0.830
16      fbs_1.0  categorical         2       0.170
17      fbs_nan  categorical         1       0.000
18  restecg_0.0  categorical         2       0.505
19  restecg_1.0  categorical         2       0.014
20  restecg_2.0  categorical         2       0.481
21  restecg_nan  categorical         1       0.000
22    exang_0.0  categorical         2       0.670
23    exang_1.0  categorical         2       0.330
24    exang_nan  categorical         1       0.000
25     thal_3.0  categorical         2       0.571
26     thal_6.0  categorical         2       0.071
27     thal_7.0  categorical         2       0.349
28     thal_nan  categorical         2       0.009
# Global feature importance.
ucihd_ebm_global.visualize().write_html("/tmp/ucihd_ebm_feat_imp.html", include_plotlyjs=False)

# Global contribution on age.
fid = ucihd_ebm_global.selector.Name.tolist().index("age")
ucihd_ebm_global.visualize(fid).write_html("/tmp/ucihd_ebm_age_imp.html", include_plotlyjs=False)

# Global contribution on trestbps.
fid = ucihd_ebm_global.selector.Name.tolist().index("trestbps")
ucihd_ebm_global.visualize(fid).write_html("/tmp/ucihd_ebm_trestbps_imp.html", include_plotlyjs=False)

# Global contribution on sex.
fid = ucihd_ebm_global.selector.Name.tolist().index("sex_0.0")
ucihd_ebm_global.visualize(fid).write_html("/tmp/ucihd_ebm_sex_imp.html", include_plotlyjs=False)
Global Feature Importance

As we discussed earlier the global feature importance score is represented by the standard deviation of the feature function output. Features marked more important means that they are more “active” in shaping the model decision.15

Feature Shaping: Age
Feature Shaping: Resting Blood Pressure
Feature Shaping: Gender (Female)

Local Explanation

More importantly, we must be able to explain a specific model prediction locally. EBM is inherently able to do exactly that. Using interpret this can be done easily with a couple of lines:

# Explain the same instances previously on RF.
ucihd_exp_ind = np.array([ucihd_rf_tp_idx[0], ucihd_rf_fp_idx[0]])

# We can feed multiple examples at the same time.
ucihd_ebm_local = ucihd_ebm.explain_local(
  ucihd_X_test.iloc[ucihd_exp_ind,:], ucihd_y_test[ucihd_exp_ind])
ucihd_ebm_local.visualize(0).write_html("/tmp/ucihd_ebm_exp_tp.html", include_plotlyjs=False)
ucihd_ebm_local.visualize(1).write_html("/tmp/ucihd_ebm_exp_fp.html", include_plotlyjs=False)

For the false positive case made by both RF and GBT, EBM is able to correctly predict the negative label. We still see a positive ca value contribute a lot toward a positive prediction, while EBM is able to also pick up several negative factors that jointly negate the positive impact, ending up with a correct prediction toward negative.

8 From Explanation to Trust

Throughout all the exercises above we only demonstrate limited local actual examples, so nothing really conclusive here as which model is more reasonable for each problem in making their decision. But with more investigation there may be more insights on which model can be trusted more than the others.

We summarize the benefit of explanation modeling here. In general it allows us…

  1. To reason the model behavior at a single instance level
  2. To investigate unreasonable behavior such that we can further improve the original model with feature engineering
  3. To differentiate different models with similar testing scores
  4. To build trust on a model, especially for the end user, to better formulate the subsequent action item

9 References

Abadi, Martı́n, Ashish Agarwal, Paul Barham, Eugene Brevdo, Zhifeng Chen, Craig Citro, Greg S. Corrado, et al. 2015. “TensorFlow: Large-Scale Machine Learning on Heterogeneous Systems.” http://tensorflow.org/.

Lipovetsky, Stan, and Michael Conklin. 2001. “Analysis of Regression in Game Theory Approach.” Applied Stochastic Models in Business and Industry 17 (4): 319–30.

Lou, Yin, Rich Caruana, and Johannes Gehrke. 2012. “Intelligible Models for Classification and Regression.” In Proceedings of the 18th Acm Sigkdd International Conference on Knowledge Discovery and Data Mining, 150–58. ACM.

Lou, Yin, Rich Caruana, Johannes Gehrke, and Giles Hooker. 2013. “Accurate Intelligible Models with Pairwise Interactions.” In Proceedings of the 19th Acm Sigkdd International Conference on Knowledge Discovery and Data Mining, 623–31. ACM.

Lundberg, Scott M, Gabriel G Erion, and Su-In Lee. 2018. “Consistent Individualized Feature Attribution for Tree Ensembles.” arXiv Preprint arXiv:1802.03888.

Lundberg, Scott M, and Su-In Lee. 2017. “A Unified Approach to Interpreting Model Predictions.” In Advances in Neural Information Processing Systems 30, edited by I. Guyon, U. V. Luxburg, S. Bengio, H. Wallach, R. Fergus, S. Vishwanathan, and R. Garnett, 4765–74. Curran Associates, Inc. http://papers.nips.cc/paper/7062-a-unified-approach-to-interpreting-model-predictions.pdf.

Maas, Andrew L., Raymond E. Daly, Peter T. Pham, Dan Huang, Andrew Y. Ng, and Christopher Potts. 2011. “Learning Word Vectors for Sentiment Analysis.” In Proceedings of the 49th Annual Meeting of the Association for Computational Linguistics: Human Language Technologies, 142–50. Portland, Oregon, USA: Association for Computational Linguistics. http://www.aclweb.org/anthology/P11-1015.

Nori, Harsha, Samuel Jenkins, Paul Koch, and Rich Caruana. 2019. “InterpretML: A Unified Framework for Machine Learning Interpretability.” arXiv Preprint arXiv:1909.09223.

Pedregosa, F., G. Varoquaux, A. Gramfort, V. Michel, B. Thirion, O. Grisel, M. Blondel, et al. 2011. “Scikit-Learn: Machine Learning in Python.” Journal of Machine Learning Research 12: 2825–30.

Pennington, Jeffrey, Richard Socher, and Christopher Manning. 2014. “Glove: Global Vectors for Word Representation.” In Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (Emnlp), 1532–43.

Ribeiro, Marco Tulio, Sameer Singh, and Carlos Guestrin. 2016. “Why Should I Trust You?: Explaining the Predictions of Any Classifier.” In Proceedings of the 22nd Acm Sigkdd International Conference on Knowledge Discovery and Data Mining, 1135–44. ACM.

Ushey, Kevin, JJ Allaire, and Yuan Tang. 2019. Reticulate: Interface to ’Python’. https://CRAN.R-project.org/package=reticulate.


  1. Some people will further differentiate explainability from interpretability, by characterizing interpretability as knowing how without knowing why, and explainability as not only knowing how but also knowing why. In this notebook for simplicity we don’t take such approach.↩︎

  2. In the R package iml the default distance function for LIME is Gower’s distance, which is designed to handle distance in data with mixed types.↩︎

  3. Keras also comes with the dataset preprocessed as integer sequences (from tf.keras.datasets import imdb).↩︎

  4. It will be much faster if we choose xgboost’s or lightgbm’s implementation of random forest. However, to demonstrate compatibility of lime with scikit-learn we purposely choose the slower implementation here.↩︎

  5. Note that, even for a single recurrent layer, training a RNN will be prohibitively slow without a GPU.↩︎

  6. V.A. Medical Center, Long Beach and Cleveland Clinic Foundation:Robert Detrano, M.D., Ph.D.↩︎

  7. xgboost is the first to introduce such missing treatment among all the GBT package. lightgbm follows.↩︎

  8. In their original work Shapley value is used to explain a large scale regression problem. But the concpet can easily extend to any machine learning model not limited to a regression model.↩︎

  9. Such sampling scheme actually assumes independence among features, which is in general not true. More sophisticated sampling approach can be applied, but that will be out of our scope in this notebook.↩︎

  10. It is also buggy indeed. With the newest version we still encounter sparse input error and additivity check failure.↩︎

  11. By default lightgbm calculates the importance by counting how many times a feature contributes to an optimal split during training. It also supports the impurity-based approach with argument importance_type set to "gain".↩︎

  12. https://github.com/slundberg/shap/issues/850.↩︎

  13. This is referred to as histogram tree approach and is also adopted in LightGBM’s implementation of gradient boosting trees. Experiments suggest such approach greatly reduce training time without compromising model accuracy.↩︎

  14. To use FAST algorithm to detect interactions we need to pass explicitly an integer argument interactions to the constructor. By default interactions=0, i.e., no interaction is actually estimated at all. After several experiments it seems that in this particular case interactions does not help improve the model. So we stick to the default setting.↩︎

  15. Feature shaping plot for interaction will be plotted as a heatmap. We dind’t demo that since interaction didn’t help our model at all.↩︎

LS0tCnRpdGxlOiAiT24gTW9kZWwgRXhwbGFpbmFiaWxpdHkiCnN1YnRpdGxlOiAiRnJvbSBMSU1FLCBTSEFQLCB0byBFeHBsYWluYWJsZSBCb29zdGluZyIKYXV0aG9yOgotIG5hbWU6IEt5bGUgQ2h1bmcKICBhZmZpbGlhdGlvbjoKZGF0ZTogImByIGZvcm1hdChTeXMudGltZSgpLCAnJWQgJWIgJVknKWAgTGFzdCBVcGRhdGVkICgwOSBEZWMgMjAxOSBGaXJzdCBVcGxvYWRlZCkiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgaGlnaGxpZ2h0OiB0YW5nbwogICAgbnVtYmVyX3NlY3Rpb25zOiB5ZXMKICAgIHRoZW1lOiBwYXBlcgogICAgdG9jOiB5ZXMKICAgIHRvY19kZXB0aDogNAogICAgdG9jX2Zsb2F0OiB5ZXMKICAgIGluY2x1ZGVzOgogICAgICBpbl9oZWFkZXI6IC90bXAvbWV0YV9oZWFkZXIuaHRtbAogIGNvZGVfZG93bmxvYWQ6IHRydWUKYmlibGlvZ3JhcGh5OiBtb2RlbF9leHBsYWluLmJpYgpub2NpdGU6IHwKICBAcmV0aWN1bGF0ZQogIEBtYWFzLUV0QWw6MjAxMTpBQ0wtSExUMjAxMQogIEBzY2lraXQtbGVhcm4KICBAdGVuc29yZmxvdzIwMTUtd2hpdGVwYXBlcgphYnN0cmFjdDogfAogIE1vZGVsIGV4cGxhaW5hYmlsaXR5IGhhcyBnYWluZWQgbW9yZSBhbmQgbW9yZSBhdHRlbnRpb24gcmVjZW50bHkgYW1vbmcgbWFjaGluZSBsZWFybmluZyBwcmFjdGl0aW9uZXJzLiBFc3BlY2lhbGx5IHdpdGggdGhlIHBvcHVsYXJpemF0aW9uIG9mIGRlZXAgbGVhcm5pbmcgZnJhbWV3b3Jrcywgd2hpY2ggZnVydGhlciBwcm9tb3RlcyB0aGUgdXNlIG9mIGluY3JlYXNpbmdseSBjb21wbGljYXRlZCBtb2RlbHMgdG8gaW1wcm92ZSBhY2N1cmFjeS4gSW4gdGhlIHJlYWxpdHksIGhvd2V2ZXIsIG1vZGVsIHdpdGggdGhlIGhpZ2hlc3QgYWNjdXJhY3kgbWF5IG5vdCBiZSB0aGUgb25lIHRoYXQgY2FuIGJlIGRlcGxveWVkLiBUcnVzdCBpcyBvbmUgaW1wb3J0YW50IGZhY3RvciBhZmZlY3RpbmcgdGhlIGFkb3B0aW9uIG9mIGNvbXBsaWNhdGVkIG1vZGVscy4gSW4gdGhpcyBub3RlYm9vayB3ZSBnaXZlIGEgYnJpZWYgaW50cm9kdWN0aW9uIHRvIHNldmVyYWwgcG9wdWxhciBtZXRob2RzIG9uIG1vZGVsIGV4cGxhaW5hYmlsaXR5LiBBbmQgd2UgZm9jdXMgbW9yZSBvbiB0aGUgaGFuZHMtb24gd2hpY2ggZGVtb25zdHJhdGVzIGhvdyB3ZSBjYW4gYWN0dWFsbHkgZXhwbGFpbiBhIG1vZGVsLCB1bmRlciBhIHZhcmlldHkgb2YgbW9kZWwgZmFtaWxpZXMuCi0tLQo8IS0tIEVtYmVkIGxpbWUgamF2YXNjcmlwdCBsaWJyYXJ5IGZvciBleHBsYW5hdGlvbiB2aXN1YWxpemF0aW9uLgogIFRoZSBzb3VyY2UgZmlsZSwgaWYgbm90IGZvdW5kLCBpcyBwcm9ncmFtbWF0aWNhbGx5IGdlbmVyYXRlZCB3aXRoaW4gdGhlIG5vdGVib29rIGNodW5rcy4KLS0+CjxzY3JpcHQgc3JjPSJsaW1lLmpzIj48L3NjcmlwdD4KCjwhLS0gRW1iZWQgcGxvdGx5IGphdmFzY3JpcHQgbGlicmFyeS4KICBUaGlzIGlzIHRoZSBiYWNrZW5kIGZvciBpbnRlcnByZXRNTCB2aXN1YWxpemF0aW9uLgotLT4KPHNjcmlwdCBzcmM9Ii4uLy4uLy4uL3NpdGVfbGlicy91dGlscy9wbG90bHktMS41MS4xLm1pbi5qcyI+PC9zY3JpcHQ+CgpgYGB7ciBtZXRhLCBpbmNsdWRlPUZBTFNFfQptZXRhX2hlYWRlcl9maWxlIDwtIGZpbGUoIi90bXAvbWV0YV9oZWFkZXIuaHRtbCIpCgojIEFkZCBvcGVuIGdyYXBoIG1ldGEuCm1ldGEgPC0gYygKICAnPG1ldGEgbmFtZT0iYXV0aG9yIiBjb250ZW50PSJLeWxlIENodW5nIj4nLAogICc8bWV0YSBwcm9wZXJ0eT0ib2c6dGl0bGUiIGNvbnRlbnQ9Ik9uIE1vZGVsIEV4cGxhaW5hYmlsaXR5OiBGcm9tIFNoYXAsIExpbWUsIHRvIEludGVycHJldGFibGUgQm9vc3RpbmciPicsCiAgJzxtZXRhIHByb3BlcnR5PSJvZzp0eXBlIiBjb250ZW50PSJhcnRpY2xlIj4nLAogICc8bWV0YSBwcm9wZXJ0eT0ib2c6dXJsIiBjb250ZW50PSJodHRwczovL2V2ZXJkYXJrLmdpdGh1Yi5pby9rOS9ub3RlYm9va3MvbWwvbW9kZWxfZXhwbGFpbi9tb2RlbF9leHBsYWluLm5iLmh0bWwiPicsCiAgJzxtZXRhIHByb3BlcnR5PSJvZzppbWFnZSIgY29udGVudD0iaHR0cHM6Ly9ldmVyZGFyay5naXRodWIuaW8vazkvYXNzZXRzL2FuZHJvaWRpZnkuanBnIj4nLAogICc8bWV0YSBwcm9wZXJ0eT0ib2c6ZGVzY3JpcHRpb24iIGNvbnRlbnQ9IkEgZGF0YSBzY2llbmNlIG5vdGVib29rIGFib3V0IG1hY2hpbmUgbGVhcm5pbmcgbW9kZWwgZXhwbGFpbmFiaWxpdHkuIj4nCikKY29udGVudHMgPC0gbWV0YQoKIyBBZGQgR2l0aHViIGNvcm5lci4KZ2l0aHViX2Nvcm5lcl9zdmcgPC0gIi4uLy4uLy4uL2Fzc2V0cy9naXRodWJfY29ybmVyLmh0bWwiCmdpdGh1Yl9jb3JuZXJfY29uZiA8LSBsaXN0KGdpdGh1Yl9saW5rPSJodHRwczovL2dpdGh1Yi5jb20vZXZlcmRhcmsvazkvdHJlZS9tYXN0ZXIvbm90ZWJvb2tzL21sL21vZGVsX2V4cGxhaW4iKQpjb250ZW50cyA8LSBjKGNvbnRlbnRzLCBzdHJpbmdyOjpzdHJfaW50ZXJwKHJlYWRMaW5lcyhnaXRodWJfY29ybmVyX3N2ZyksIGdpdGh1Yl9jb3JuZXJfY29uZikpCndyaXRlTGluZXMoY29udGVudHMsIG1ldGFfaGVhZGVyX2ZpbGUpCgpjbG9zZShtZXRhX2hlYWRlcl9maWxlKQpgYGAKCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQpsaWJyYXJ5KHJldGljdWxhdGUpCnIgPC0gdHJ5KHVzZV9weXRob24oU3lzLmdldGVudigiUFlUSE9OX1BBVEgiKSwgcmVxdWlyZWQ9VFJVRSksIHNpbGVudD1UUlVFKQppZiAoIGlzKHIsICJ0cnktZXJyb3IiKSApIHsKICByIDwtIHRyeSh1c2VfdmlydHVhbGVudihTeXMuZ2V0ZW52KCJQWVRIT05fUEFUSCIpLCByZXF1aXJlZD1UUlVFKSwgc2lsZW50PVRSVUUpCiAgaWYgKCBpcyhyLCAidHJ5LWVycm9yIikgKSB1c2VfY29uZGFlbnYoU3lzLmdldGVudigiUFlUSE9OX1BBVEgiKSwgcmVxdWlyZWQ9VFJVRSkKfQoKIyBVdGlsaXR5IHRvIHBvc3QtcHJvY2VzcyBodG1sIG91dHB1dC4KbGlicmFyeSh4bWwyKQoKIyBGb3Igc29tZSBwbG90cy4KbGlicmFyeShnZ3Bsb3QyKQoKd3JpdGVfbGltZV9qcyA8LSBmdW5jdGlvbihpbmZpbGUpIHsKICAjIGxpbWUgaHRtbCBvdXRwdXQgY29udGFpbnMgYSBodWdlIGpzIHN0cmluZywKICAjIHRvIHJlZHVjZSBub3RlYm9vayBmaWxlIHNpemUgd2Ugb25seSB3YW50IHRvIGRlY2xhcmUgdGhlIGpzIG9uY2UuCiAgb3V0ZmlsZSA8LSAibGltZS5qcyIKICBkb2MgPC0gYXNfbGlzdChyZWFkX2h0bWwoaW5maWxlKSkKICBqc19zdHIgPC0gZG9jJGh0bWwkaGVhZCRzY3JpcHRbWzFdXQogICMgVXNlIGg1IGZvciB0ZXh0IGV4YW1wbGUgaGVhZGVyIHRvIGF2b2lkIGJlaW5nIGluY2x1ZGVkIGluIHJtZCB0b2MuCiAganNfc3RyIDwtIGdzdWIoImgzIiwgImg1IiwganNfc3RyKQogIHdyaXRlTGluZXMoanNfc3RyLCBvdXRmaWxlLCB1c2VCeXRlcz1UUlVFKQp9CgpwYXJzZV9saW1lX2h0bWxfb3V0cHV0IDwtIGZ1bmN0aW9uKGluZmlsZSwgZXhjbHVkZV9qcz1UUlVFKSB7CiAgb3V0ZmlsZSA8LSB0ZW1wZmlsZSgpCiAgZG9jIDwtIHJlYWRfaHRtbChpbmZpbGUpCiAgaWYgKCBleGNsdWRlX2pzICkgeG1sX3JlbW92ZSh4bWxfY2hpbGQoZG9jKSkKICB3cml0ZV9odG1sKGRvYywgb3V0ZmlsZSkKICBvdXRmaWxlCn0KYGBgCgotLS0KClRoaXMgbm90ZWJvb2sgaXMgd3JpdHRlbiB3aXRoIFtgcmV0aWN1bGF0ZWBdKGh0dHBzOi8vZ2l0aHViLmNvbS9yc3R1ZGlvL3JldGljdWxhdGUpLAphIHBhY2thZ2UgdGhhdCBhbGxvd3MgaW50ZXItb3BlcmF0aW9uIGJldHdlZW4gUiBhbmQgUHl0aG9uLgoKLS0tCgojIE1vdGl2YXRpb24KCldoeSBkbyB3ZSBuZWVkIHRvIGV4cGxhaW4gYSBtYWNoaW5lIGxlYXJuaW5nIG1vZGVsPwpUaGUgYmVuZWZpdCBvZiBhbiBleHBsYW5hYmxlIG1vZGVsIGFnYWluc3QgYSBibGFjay1ib3ggbW9kZWwgaXMgZm9yIHRoZSBtb2RlbCB0byBiZSAqdHJ1c3RlZCouClRydXN0IGNhbiBiZSBpbXBvcnRhbnQgaW4gbWFueSByZWFsIGFwcGxpY2F0aW9ucyB3aGVyZSB0aGUgc3VjY2Vzc2Z1bCBkZXBsb3ltZW50IG9mIGEgbWFjaGluZSBsZWFybmluZyBtb2RlbCByZXF1aXJlcyB0aGUgdHJ1c3QgZnJvbSBlbmQgdXNlcnMuClNvbWV0aW1lcyB0cnVzdCBwbGF5cyBhIGV2ZW4gYmlnZ2VyIHJvbGUgdGhhbiBtb2RlbCBhY2N1cmFjeS4KCk90aGVyIHRoYW4gdHJ1c3QsCm1vZGVsIGV4cGxhaW5hYmlsaXR5IChvciBpbnRlcnByZXRhYmlsaXR5LCBpbnRlcmNoYW5nZWFibHkgdXNlZCBoZXJlYWZ0ZXIpIG1heSBhbHNvIGd1aWRlIHVzIGluIHRoZSBjb3JyZWN0IGRpcmVjdGlvbiB0byBmdXJ0aGVyIGltcHJvdmUgdGhlIG1vZGVsLgpeW1NvbWUgcGVvcGxlIHdpbGwgZnVydGhlciBkaWZmZXJlbnRpYXRlIGV4cGxhaW5hYmlsaXR5IGZyb20gaW50ZXJwcmV0YWJpbGl0eSwKYnkgY2hhcmFjdGVyaXppbmcgaW50ZXJwcmV0YWJpbGl0eSBhcyBrbm93aW5nIGhvdyB3aXRob3V0IGtub3dpbmcgd2h5LAphbmQgZXhwbGFpbmFiaWxpdHkgYXMgbm90IG9ubHkga25vd2luZyBob3cgYnV0IGFsc28ga25vd2luZyB3aHkuCkluIHRoaXMgbm90ZWJvb2sgZm9yIHNpbXBsaWNpdHkgd2UgZG9uJ3QgdGFrZSBzdWNoIGFwcHJvYWNoLl0KCkluIGdlbmVyYWwsCmxpbmVhciBtb2RlbCBpcyBtb3JlIGludGVycHJldGFibGUgdGhhbiBub24tbGluZWFyIG1vZGVsLgpCdXQgdGhlIGZvcm1lciBhbHNvIHN1ZmZlcnMgZnJvbSBsb3dlciBhY2N1cmFjeS4KTW9yZSBhZHZhbmNlZCBhbmQgaGVuY2UgY29tcGxpY2F0ZWQgbW9kZWwgdXN1YWxseSBoYXMgd29yc2UgaW50ZXJwcmV0YWJpbGl0eS4KCk9uZSBzaG91bGQgbm90IGNvbmZ1c2UgbW9kZWwgZXhwbGFpbmFiaWxpdHkgd2l0aCB0aGUgYWN0dWFsIGNhdXNhbGl0eS4KQmVpbmcgYWJsZSB0byBleHBsYWluIGEgbW9kZWwgZG9lc24ndCBtZWFuIHRoYXQgd2UgY2FuIGlkZW50aWZ5IGFueSBncm91bmQtdHJ1dGggY2F1c2FsIHJlbGF0aW9uIGJlaGluZCB0aGUgbW9kZWwuCk1vZGVsIGV4cGxhaW5hYmlsaXR5IGlzIGZvciBhbmQgb25seSBmb3IgdGhlIG1vZGVsLApidXQgbm90IGZvciB0aGUgZmFjdHMgd2UnZCBsaWtlIHRvIG1vZGVsLgpOZXZlcnRoZWxlc3MsCnVuZGVyc3RhbmQgaG93IHdlIGNhbiByZWFzb24gdGhlIG1vZGVsIGRlZmluaXRlbHkgd2lsbCBoZWxwIHVzIGJldHRlciBtb2RlbCB0aGUgYWN0dWFsIHBhdHRlcm4gYmVoaW5kIHRoZSBzY2VuY2UuCgojIE9wZW4gU291cmNlIExpYnJhcmllcyBmb3IgTW9kZWwgRXhwbGFuYXRpb24KCkluIHRoaXMgbm90ZWJvb2sgd2Ugd2lsbCB3YWxrIHRocm91Z2ggMyBwb3B1bGFyIGFwcHJvYWNoZXMgb2YgbW9kZWwgcHJlZGljdGlvbiBleHBsYW5hdGlvbiwKZWFjaCBvZiB0aGVtIGNvbWVzIHdpdGggYSBkZWRpY2F0ZWQgUHl0aG9uIHBhY2thZ2U6CgoxLiBbYHNoYXBgXShodHRwczovL2dpdGh1Yi5jb20vc2x1bmRiZXJnL3NoYXApIGZvciBTSEFQCjIuIFtgbGltZWBdKGh0dHBzOi8vZ2l0aHViLmNvbS9tYXJjb3Rjci9saW1lKSBmb3IgTElNRQozLiBbYGludGVycHJldGBdKGh0dHBzOi8vZ2l0aHViLmNvbS9pbnRlcnByZXRtbC9pbnRlcnByZXQpIGZvciBFeHBsYWluYWJsZSBCb29zdGluZyBNYWNoaW5lCgpBbGwgY29kaW5nIGV4YW1wbGVzIHdpbGwgYmUgYmFzZWQgb24gdGhlIGFib3ZlIDMgcGFja2FnZXMuCldoYXQgaWYgd2UnZCBsaWtlIHRvIGV4cGxvcmUgdGhlc2UgYXBwcm9hY2hlcyBpbiBSPwpGb3IgYHNoYXBgLAp0aGVyZSBpcyBhIFIgcG9ydCBjYWxsZWQgW2BzaGFwcGVyYF0oaHR0cHM6Ly9naXRodWIuY29tL01vZGVsT3JpZW50ZWQvc2hhcHBlcikuCkZvciBgbGltZWAgd2UgYWxzbyBoYXZlIFtgaW1sYF0oaHR0cHM6Ly9naXRodWIuY29tL2NocmlzdG9waE0vaW1sKSB3aGljaCBjb250YWlucyBtb3JlIHRoYW4ganVzdCBMSU1FIGFwcHJvYWNoLgpBcyBmb3IgYGludGVycHJldGAsCml0IGFscmVhZHkgY29tZXMgd2l0aCBbaXRzIG93biBSIEFQSV0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL2ludGVycHJldC9pbmRleC5odG1sKS4KCiMgRXhwbGFuYXRpb24gTW9kZWxzCgpBbiBleHBsYW5hdGlvbiBtb2RlbCAkZyh4XHByaW1lKSQgaXMgYW4gKmludGVycHJldGFibGUgYXBwcm94aW1hdGlvbiogb2YgdGhlIG9yaWdpbmFsIG1vZGVsICRmKHgpJC4KSXRzIHNvbGUgcHVycG9zZSBpcyB0byBnaXZlIGV4dHJhIGV4cGxhaW5hYmlsaXR5IHRoZSBvcmlnaW5hbCBtb2RlbCBmYWlscyB0byBwcm92aWRlLApkdWUgdG8gaXRzIG93biBjb21wbGV4aXR5LgoKVGhlIGdlbmVyYWwgaWRlYSBpcyB0byB1c2UgYSBzaW1wbGlmaWVkIGlucHV0ICR4XHByaW1lJCBzdWNoIHRoYXQgJHggPSBoX3goeFxwcmltZSkkLAp3aGVyZSAkaF94KFxjZG90KSQgaXMgYSBtYXBwaW5nIGZ1bmN0aW9uIGZvciBhbnkgZ2l2ZW4gcmF3IGlucHV0ICR4JC4KVGhlbiB0aGUgaW50ZXJwcmV0YWJsZSBhcHByb3hpbWF0aW9uIGNhbiBiZSB3cml0dGVuIGFzOgoKJCQKZyh4XHByaW1lKSBcYXBwcm94IGYoaF94KHhccHJpbWUpKS4KJCQKClRoZSAqYWRkaXRpdmUgZmVhdHVyZSBhdHRyaWJ1dGlvbiBtZXRob2RzKiBzcGVjaWZ5IHRoZSBleHBsYW5hdGlvbiBtb2RlbCBvZiB0aGUgZm9sbG93aW5nIGZvcm06CgokJApnKHpccHJpbWUpID0gXHBoaV8wICsgXHN1bV97aiA9IDF9Xm4gXHBoaV9pIHpfaVxwcmltZSwKJCQKCndoZXJlICRuJCBpcyB0b3RhbCBudW1iZXIgb2Ygc2ltcGxpZmllZCBmZWF0dXJlcywKJHpccHJpbWUgXGluIFx7MCwgMVx9JCBzaW1wbHkgYW4gaW5kaWNhdG9yLgpJbiBtYW55IHN1Y2ggbWV0aG9kcywKdGhlIHNpbXBsaWZpZWQgaW5wdXQgaXMgdGhlIGluZGljYXRvciBvZiBmZWF0dXJlIHByZXNlbmNlLgpBcHBhcmVudGx5LAp0aGUgY2hvaWNlIG9mIGFuIGFkZGl0aXZlIG1vZGVsIGlzIGZvciAobGluZWFyKSBpbnRyZXByZXRhYmlsaXR5LgpUaGUgc2ltcGxpZmllZCBmZWF0dXJlcyBhcmUgYW4gKmludGVycHJldGFibGUgcmVwcmVzZW50YXRpb24qIG9mIHRoZSBvcmlnaW5hbCBtb2RlbCBmZWF0dXJlcy4KCkFzIHdlIHdpbGwgc2VlIGFkZGl0aXZpdHkgaXMgdGhlIGtleSB0byBleHBsYWluYWJpbGl0eS4KQWxsIHRoZSBhcHByb2FjaGVzIHdlIHdpbGwgZGlzY3VzcyBpbiB0aGlzIG5vdGVib29rIGZvbGxvdyB0aGlzIHBoaWxvc29waHkuCgojIExJTUUKCk9uZSB2ZXJ5IHBvcHVsYXIgc3VjaCBhYm92ZSBhZGRpdGl2ZSBtb2RlbCBpcyBMSU1FIChAcmliZWlybzIwMTZzaG91bGQpLgpMSU1FIHN0YW5kcyBmb3IgKipMb2NhbCBJbnRlcnByZXRhYmxlIE1vZGVsLUFnbm9zdGljIEV4cGxhbmF0aW9ucy4qKgpBcyBpdHMgZnVsbCBuYW1lIHN1Z2dlc3RzLApMSU1FIGNhbiBiZSBhcHBsaWVkIHRvICphbnkqIG1hY2hpbmUgbGVhcm5pbmcgbW9kZWwuCkxJTUUgYWNoaWV2ZXMgcHJlZGljdGlvbi1sZXZlbCBpbnRlcnByZXRhYmlsaXR5IGJ5IGFwcHJveG1pYXRpbmcgdGhlIG9yaWdpbmFsIG1vZGVsIHdpdGggYW4gZXhwbGFuYXRpb24gbW9kZWwgbG9jYWxseSBhcm91bmQgdGhhdCBwcmVkaWN0aW9uLgoKRnJvbSB0aGVpciBvcmlnaW5hbCBwYXBlcjoKCj4gQnkg4oCcZXhwbGFpbmluZyBhIHByZWRpY3Rpb27igJ0sCndlIG1lYW4gcHJlc2VudGluZyB0ZXh0dWFsIG9yIHZpc3VhbCBhcnRpZmFjdHMgdGhhdCBwcm92aWRlIHF1YWxpdGF0aXZlIHVuZGVyc3RhbmRpbmcgb2YgdGhlIHJlbGF0aW9uc2hpcCBiZXR3ZWVuIHRoZSBpbnN0YW5jZeKAmXMgY29tcG9uZW50cyAoZS5nLiB3b3JkcyBpbiB0ZXh0LCBwYXRjaGVzIGluIGFuIGltYWdlKSBhbmQgdGhlIG1vZGVs4oCZcyBwcmVkaWN0aW9uLgoKRmVhdHVyZSBzcGFjZSBpbiB0aGUgb3JpZ2luYWwgbW9kZWwgd2lsbCBiZSBpbiBnZW5lcmFsIGRpZmZlcmVudCBmcm9tIHRoYXQgb2YgdGhlIGV4cGxhbmF0aW9uIG1vZGVsLgpBbiBleHBsYW5hdGlvbiBtb2RlbCB3aWxsIHVzZSBpbnRlcnByZXRhYmxlIHJlcHJlc2VudGF0aW9uIG9mIHRoZSBvcmlnaW5hbCBmZWF0dXJlIGFzIHRoZWlyIHRyYWluaW5nIGlucHV0LgpEaWZmZXJlbnQgZGF0YSB0eXBlIHdpbGwgaGF2ZSB0aGVpciBkaWZmZXJlbnQgaW50ZXJwcmV0YWJsZSByZXByZXNlbnRhdGlvbi4KCiMjIEJpbmFyaXplZCBJbnRlcnByZXRhYmxlIEZlYXR1cmUgU3BhY2UKCkxJTUUgcHJvcG9zZXMgYW4gZXhwbGFuYXRpb24gbW9kZWwgJGcoeFxwcmltZSkkIHdpdGggYSBkb21haW4gJFx7MCwgMVx9JC4KVGhhdCBpcywgaXQgYWN0cyBvbiBhYnNlbmNlIG9yIHByZXNlbmNlIG9mIHRoZSBpbnRlcnByZXRhYmxlIGZlYXR1cmVzICR4XHByaW1lJC4KVGhlIGNob2ljZSBpcywgb2J2aW91c2x5LCBmb3IgYmV0dGVyIGludGVycHJldGFiaWxpdHkuCgoqKkxhbmd1YWdlIERhdGEqKgoKRm9yIHRleHQgY2xhc3NpZmljYXRpb24gcHJvYmxlbXMgd2l0aCBodW1hbiBsYW5ndWFnZSBhcyBzb3VyY2UgaW5wdXQsCnRoZSBtb3N0IHN0cmFpZ2h0Zm9yd2FyZCBpbnRlcnByZXRhYmxlIHJlcHJlc2VudGF0aW9uIHdpbGwgYmUgKmEgYmluYXJ5IGluZGljYXRvciB2ZWN0b3Igb2YgYmFnIG9mIHdvcmRzLioKU28gdGhlIGV4cGxhbmF0aW9uIG1vZGVsIHdpbGwgdHJ5IHRvIHJlYXNvbiB3aGljaCB3b3JkIG9yIHRva2VuIGlzIGRyaXZpbmcgdGhlIHByZWRpY3Rpb24gaW4gd2hhdCBkaXJlY3Rpb24uCkFuZCB0aGlzIGlzIHRydWUgbm8gbWF0dGVyIHRoZSBmb3JtIG9mIHRoZSBvcmlnaW5hbCBtb2RlbCBmZWF0dXJlLgpNYXkgaXQgYmUgYSB3b3JkIGNvdW50IG1hdHJpeCwKYSB0ZXJtIGZyZXF1ZW5jeS1pbnZlcnNlIGRvY3VtZW50IGZyZXF1ZW5jeSAoVEYtSURGKSBtYXRyaXgsCm9yIG51bWVyaWNhbCBlbWJlZGRpbmdzLgoKKipJbWFnZSBEYXRhKioKCkZvciBpbWFnZSB0YXNrcywKdGhlIGludGVycHJldGFibGUgcmVwcmVzZW50YXRpb24gaXMgKmEgam9pbnQgc2V0IG9mIGNvbnRpZ3VvdXMgc3VwZXJwaXhlbHMgdGhhdCBkaXZpZGUgdGhlIG9yaWdpbmFsIGltYWdlIGludG8gcGllY2VzLioKQSBzdXBlcnBpeGVsIGlzIGEgZ3JvdXAgb2YgcGl4ZWxzIHdpdGggc2ltaWxhciBjaGFyYWN0ZXJpc3RpY3MuClNvIGluIHBsYWluIHdvcmRzLCBqdXN0IGxpa2Ugd2Ugc2VnbWVudCBzZW50ZW5jZSBpbnRvIHRva2VucywKd2Ugc2ltcGx5IHNlZ21lbnQgaW1hZ2UgaW50byBtdWx0aXBsZSBzbWFsbCBwaWVjZXMgYW5kIGFnYWluLAp1c2UgYSBiaW5hcnkgdmVjdG9yIHRvIGluZGljYXRlIHRoZSBhYnNlbmNlIG9yIHByZXNlbmNlIG9mIGVhY2ggcGllY2UgZm9yIGxhdHRlciBwZXJ0dXJiYXRpb24gcHVycG9zZS4KCioqTnVtZXJpY2FsIERhdGEqKgoKVEJDLgoKIyMgTG9jYWwgU2FtcGxpbmcgKFBlcnR1cmJhdGlvbikKCkluIG9yZGVyIHRvIGVzdGltYXRlIHRoZSBleHBsYW5hdGlvbiBtb2RlbCBnaXZlbiBhIHByZWRpY3Rpb24gZm9yIGEgdGFyZ2V0IGV4YW1wbGUsCmZlYXR1cmVzIHRyYW5zZm9ybWVkIGludG8gYSBpbnRlcnByZXRhYmxlIHNwYWNlIGFyZSB0aGVuIHBlcnR1cmJlZCB0byBnZW5lcmF0ZSBzaW1pbGFyIGV4YW1wbGVzIGFyb3VuZCB0aGF0IHRhcmdldCBleGFtcGxlLgpUaGlzIGlzIHJlZmVycmVkIHRvIGFzIHNhbXBsaW5nIGZvciBsb2NhbCBleHBsb3JhdGlvbi4KCkdpdmVuIGFuIGV4YW1wbGUgJHhccHJpbWUkIHRvIGJlIGV4cGxhaW5lZCwKaXRzIG5vbi16ZXJvIGludGVycHJldGFibGUgZmVhdHVyZXMgKHJlbWVtYmVyIHRoZSBzcGFjZSBoYXMgYSBkb21haW4gb2YgJFx7MCwgMVx9JCkgYXJlIHVuaWZvcm1seSBzYW1wbGVkIHRvIGdlbmVyYXRlIGl0cyBsb2NhbCBzaW1pbGFyIGV4YW1wbGUgJHpccHJpbWUkLgpTbyAkelxwcmltZSQgd2lsbCBhbHdheXMgaGF2ZSBhIHN1YnNldCAob3IgYXQgbW9zdCBlcXVhbCBzZXQpIG9mIG5vbi16ZXJvIGZlYXR1cmVzIHRoYXQgJHhccHJpbWUkIGhhcy4KClRha2UgdGV4dCBkYXRhIGZvciBpbGx1c3RyYXRpb24uCklmIGEgcGFydGljdWxhciBleGFtcGxlIGhhcyB0aGUgZm9sbG93aW5nIHRva2VucyBpbiB0aGUgaW50ZXJwcmV0YWJsZSBzcGFjZToKCmBgYApBIEIgQyBIIEkgSgpgYGAKClRoZW4gYSBwb3NzaWJsZSBsb2NhbCBzYW1wbGUgY2FuIGJlIHNvbWV0aGluZyBsaWtlOgoKYGBgCkEgQyBIIEoKYGBgCgooSW1hZ2luZSB0aG9zZSBhbHBoYWJldHMgYXJlIGFjdHVhbCB3b3JkcyBwcmVzZW50IGluIHRoZSByYXcgdGV4dCBvZiB0aGUgZXhhbXBsZS4pCgpCeSBkZWZhdWx0IGluIHRoZSBwYXBlciA1LDAwMCBzYW1wbGVzIGFyZSBnZW5lcmF0ZWQgZm9yIGVhY2ggc2luZ2xlIGV4cGxhbmF0aW9uLgpBIGh5cGVycGFyYW1ldGVyICRLJCAoZGVmYXVsdCBhdCAxMCkgaXMgdXNlZCB0byBjYXAgaG93IG1hbnkgbm9uLXplcm8gaW50ZXJwcmV0YWJsZSBmZWF0dXJlcyB3ZSdkIGxpa2UgdG8gZXN0aW1hdGUgaW4gdGhlIHN1YnNlcXVlbnQgbW9kZWwgbGVhcm5pbmcgcGhhc2UsCnRvIG5vdCBvbmx5IGtlZXAgdGhlIG1vZGVsIHNvbHZpbmcgdHJhY3RhYmxlIGJ1dCBhbHNvIG1hbmFnZWFibGUgZm9yIGh1bWFuIGludGVycHJldGF0aW9uLgoKIyMgTGVhcm5pbmcgVGFzayBvZiB0aGUgRXhwbGFuYXRpb24gTW9kZWwKCk5vdyBlYWNoIG9mIHRoZSBwZXJ0dXJiZWQgZXhhbXBsZSB3aWxsIGJlIGZpcnN0ICp0cmFuc2Zvcm1lZCBiYWNrKiB0byB0aGVpciBvcmlnaW5hbCBmZWF0dXJlIHNwYWNlLAp0aGVuIGZlZWQgaW50byB0aGUgb3JpZ2luYWwgbW9kZWwgdG8gZ2V0IHRoZSBwcmVkaWN0ZWQgbGFiZWwuClRoYXQgaXMsCmZyb20gJHpccHJpbWUkIHdlIG5lZWQgdG8gZ2V0ICRmKHopJCB3aGVyZSAkZiQgaXMgdGhlIG9yaWdpbmFsIG1vZGVsIGFuZCAkeiQgdGhlIG9yaWdpbmFsIGZlYXR1cmUgcmVwcmVzZW50YXRpb24uClRoZXNlIGxhYmVscyBzZXJ2ZSBleGFjdGx5IGFzIHRoZSBsYWJlbHMgdG8gdHJhaW4gdGhlIGxvY2FsIGV4cGxhbmF0aW9uIG1vZGVsLAp3aGVyZSBhbGwgcmFuZG9tIHBlcnR1cmJhdGlvbnMgJHokIGFyZSB3ZWlndGhlZCBieSAkXHBpX3goeikkLAphIHByb3hpbWl0eSBmdW5jdGlvbiAkXHBpX3goeikkIGNhbiBiZSBkZWZpbmVkIHRvIG1lYXN1cmUgaG93IGNsb3NlICR6JCBpcyB0byAkeCQsCmluIHRoZSBvcmlnaW5hbCBzcGFjZS4KCkluIHRoZSBvcmlnaW5hbCBwYXBlciB0aGUgcHJveGltaXR5IGZ1bmN0aW9uIGlzIHNldCB0byBiZSBhbiBleHBvbmVudGlhbCBrZXJuZWw6CgokJApccGlfeCh6KSA9IFxleHAgXGJpZ2coIFxmcmFjey1EKHgsIHopXjJ9e1xzaWdtYV4yfSBcYmlnZyksCiQkCgp3aGVyZSAkRCh4LCB6KSQgaXMgY29zaW5lIGRpc3RhbmNlIGZvciB0ZXh0IGFuZCBMMiBkaXN0YW5jZSBmb3IgaW1hZ2UsCiRcc2lnbWEkIGlzIGEgaHlwZXJwYXJhbWV0ZXIgZGVmYXVsdCBhdCAyNSBpbiBgbGltZWAuCgpUaGUgbGVhcm5lciBpcyBhIHNpbXBsZSBsaW5lYXIgbW9kZWw6CgokJApnKHhccHJpbWUpID0gVyBcY2RvdCB4XHByaW1lLgokJAoKV2UgbGVhcm4gdGhlIGV4cGxhbmF0aW9uIG1vZGVsIHdlaWdodHMgJFckIGJ5IG1pbmltaXppbmcgdGhlIHN1bSBvZiBwcm94aW1pdHktd2VpZ2h0ZWQgc3F1YXJlZCBsb3NzZXMgZm9yIGFsbCBwZXJ0dXJiZWQgbG9jYWwgc2FtcGxlczoKCiQkCkxvc3MgPSBcc3VtX3t6LCB6XHByaW1lfSBccGlfeCh6KSBcY2RvdCBcYmlnKGYoeikgLSBnKHpccHJpbWUpXGJpZyleMi4KJCQKClRoZSBhY3R1YWwgbGVhcm5pbmcgYWxnb3JpdGhtIHByb3Bvc2VkIGJ5IExJTUUgaW4gdGhlIG9yaWdpbmFsIHBhcGVyIGlzIGEgW0xBU1NPXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9MYXNzb18oc3RhdGlzdGljcykpLgpCdXQgaW4gdGhlIGFjdHVhbCBpbXBsZW1lbnRhdGlvbiBvZiB0aGUgYGxpbWVgIHBhY2thZ2UsCltSaWRnZSByZWdyZXNzaW9uXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9UaWtob25vdl9yZWd1bGFyaXphdGlvbikgaXMgdXNlZCBpbnN0ZWFkIGFzIHRoZSBkZWZhdWx0IGxlYXJuZXIuCkRlc3BpdGUgdGhpcywKdGhlIHRvcCAkSyQgZmVhdHVyZXMgZm9yIHRoZSBsZWFybmVyIGFyZSBzdGlsbCBjaG9zZW4gYnkgYSBMQVNTTyBwYXRoLgoKQW5vdGhlciBkaXNjcmVwYW5jeSBiZXR3ZWVuIHRoZSBvcmlnaW5hbCBwYXBlciBhbmQgdGhlIGFjdHVhbCBpbXBsZW1lbnRhdGlvbiBpcyB0aGUgbm90aW9uIG9mIHByb3hpbWl0eSBmdW5jdGlvbiAkXHBpX3goeikkLgpJbiB0aGUgYWN0dWFsIGltcGxlbWVudGF0aW9uIHByb3hpbWl0eSBpcyBjYWxjdWxhdGVkIGluIHRoZSBpbnRlcnByZXRhYmxlIHNwYWNlIHJhdGhlciB0aGFuIGluIHRoZSBvcmlnaW5hbCBzcGFjZS4KU28gZXNzZW50aWFsbHkgd2Ugc2hvdWxkIGhhdmUgZGVub3RlZCB0aGUgZnVuY3Rpb24gYXMgJFxwaV97eFxwcmltZX0oelxwcmltZSkkLgpJbmRlZWQsCmluIHRoZSBwYWNrYWdlIHNvdXJjZSBjb2RlIHRoZSBwcm94aW1pdHkgZnVuY3Rpb24gaXMgZGVmaW5lZCBhczoKXltJbiB0aGUgUiBwYWNrYWdlIFtgaW1sYF0oaHR0cHM6Ly9naXRodWIuY29tL2NocmlzdG9waE0vaW1sKSB0aGUgZGVmYXVsdCBkaXN0YW5jZSBmdW5jdGlvbiBmb3IgTElNRSBpcyBHb3dlcidzIGRpc3RhbmNlLAp3aGljaCBpcyBkZXNpZ25lZCB0byBoYW5kbGUgZGlzdGFuY2UgaW4gZGF0YSB3aXRoIG1peGVkIHR5cGVzLl0KCiQkClxwaV97eFxwcmltZX0oelxwcmltZSkgPSBcc3FydHsgXGV4cCBcYmlnZyggXGZyYWN7LShEKHhccHJpbWUsIHpccHJpbWUpIFx0aW1lcyAxMDApXjJ9e1xzaWdtYV4yfSBcYmlnZyl9LgokJAoKQXMgb25lIG1heSByZWFsaXplIG5vdyB0aGF0IHRoZSBvcmlnaW5hbCBtb2RlbCBjYW4gYmUgYSB0b3RhbCBibGFja2JveC4KV2Ugb25seSB1c2UgaXRzIHByZWRpY3Rpb25zIGFzIGxhYmVscyB0byBsZWFybiB0aGUgZXhwbGFuYXRpb24gbW9kZWwuCkJlIGF3YXJlIHRoYXQgaGVyZSAkZih6KSQgcmV0dXJucyB0aGUgcHJlZGljdGVkICpwcm9iYWJpbGl0eSogYXMgbGFiZWwgc28gdGhlIGV4cGxhbmF0aW9uIG1vZGVsIGlzIGEgcmVncmVzc29yIG5vdCBhIGNsYXNzaWZpZXIuCgojIyBMaW1pdGF0aW9ucwoKKipMaW5lYXJpdHkqKgoKTm90aWNlIHRoYXQgdGhlIGxvY2FsIGV4cGxhbmF0aW9uIG1vZGVsIGlzIGEgbGluZWFyIG1vZGVsLAp0aGUgZXhwbGFuYXRpb24gaGVuY2UgaXMgc3ViamVjdCB0byBsaW5lYXJpdHkuCklmIHdlIGhhdmUgYW55IGV2aWRlbmNlIHN1Z2dlc3RpbmcgaGVhdnkgbm9uLWxpbmVhcml0eSBhcm91bmQgYSBwcmVkaWN0aW9uLAp0aGUgb3V0cHV0IG9mIHN1Y2ggZXhwbGFuYXRpb24gbW9kZWwgd29uJ3QgYmUgZmFpdGhmdWwuCgoqKk5vIEV4cGxhbmF0aW9uIGZvciBhIE5VTEwgRWZmZWN0KioKCkFuZCBmb3IgdGhlIGxvY2FsIHNhbXBsaW5nLApub3RpY2UgdGhhdCB3ZSBvbmx5IHN1YnNhbXBsZSBmcm9tIHRoZSBwcmVzZW5jZSBvZiBmZWF0dXJlcyBvbiB0aGUgdGFyZ2V0IGV4YW1wbGUuClNvIHRoZSBleHBsYW5hdGlvbiBpcyBvbiB0aGUgcHJlc2VuY2Ugb3IgYWJzZW5jZSBvZiB0aGUgYW55dGhpbmcgYWN0dWFsbHkgcHJlc2VudCBpbiB0aGUgdGFyZ2V0IGV4YW1wbGUuCkEgZmVhdHVyZSB0aGF0IGlzIG9yaWdpbmFsbHkgbm90IHByZXNlbnQgaW4gdGhlIGV4YW1wbGUgY2FuIG5ldmVyIGJlIHBhcnQgb2YgdGhlIGV4cGxhbmF0aW9uLgpUaGlzIGNvdWxkIHBvdGVudGlhbGx5IG1pc3MgYW4gaW1wb3J0YW50IG51bGwgZWZmZWN0IG9mIGEgZmVhdHVyZS4KClRha2UgdGhlIHNhbWUgZXhhbXBsZSBhYm92ZToKCmBgYApBIEIgQyBIIEkgSgpgYGAKCkl0IGNvdWxkIGJlIHRoZSBjYXNlIHRoYXQsCmluc3RlYWQgb2YgdGhlIHByZXNlbmNlIG9mIHRoZSA2IGZlYXR1cmVzLAp0aGUgbWlzc2luZ25lc3Mgb2YgZmVhdHVyZSBgWmAgaXMgdGhlIG1vc3QgaW1wb3J0YW50IGRyaXZpbmcgZm9yY2UgZm9yIHRoZSBtYWNoaW5lIGxlYXJuaW5nIG1vZGVsIHRvIG1ha2UgdGhlIHByZWRpY3Rpb24uCkhvd2V2ZXIgZHVlIHRvIHRoZSBsb2NhbCBzYW1wbGluZyBzY2hlbWUsCnRoZSBpbXBvcnRhbmNlIG9mICh0aGUgbnVsbCBvZikgYFpgIGNhbiBuZXZlciBiZSBlc3RpbWF0ZWQgYnkgdGhlIGV4cGxhbmF0aW9uIG1vZGVsLgoKIyMgSGFuZHMtb24gRXhwbGFuYXRpb24gRGVtbwoKIyMjIE9uIFRleHQgQ2xhc3NpZmllcnMKCldlIHVzZSBbTGFyZ2UgTW92aWUgUmV2aWV3IERhdGFzZXRdKGh0dHBzOi8vYWkuc3RhbmZvcmQuZWR1L35hbWFhcy9kYXRhL3NlbnRpbWVudC8pIHRvIGRvIGEgYmluYXJ5IHNlbnRpbWVudCBjbGFzc2lmaWNhdGlvbiBleGVyY2lzZS4KV2Ugd2lsbCB1c2UgbWFjaGluZSBsZWFybmluZyBsaWJyYXJpZXMgc3VjaCBhcyBgc2Npa2l0LWxlYXJuYCBhbmQgYHRlbnNvcmZsb3dgIHRvIHF1aWNrbHkgYnVpbGQgYSB2YXJpZWl0eSBvZiAocmF0aGVyIGNvbXBsaWNhdGVkIGFuZCBoYXJkIHRvIGludGVycHJldCkgbW9kZWxzIGFuZCB1c2UgYGxpbWVgIHRvIGV4cGVyaW1lbnQgZXhwbGFuYXRpb24gbW9kZWxpbmcuCgpgYGB7cHl0aG9uIGltcG9ydF9zb21lfQppbXBvcnQgc3lzCnByaW50KHN5cy52ZXJzaW9uKQoKaW1wb3J0IG9zCmltcG9ydCBsb2dnaW5nCmxvZ2dpbmcuZ2V0TG9nZ2VyKCJ0ZW5zb3JmbG93Iikuc2V0TGV2ZWwobG9nZ2luZy5FUlJPUikKaW1wb3J0IHdhcm5pbmdzCndhcm5pbmdzLnNpbXBsZWZpbHRlcihhY3Rpb249Imlnbm9yZSIsIGNhdGVnb3J5PVVzZXJXYXJuaW5nKQp3YXJuaW5ncy5zaW1wbGVmaWx0ZXIoYWN0aW9uPSJpZ25vcmUiLCBjYXRlZ29yeT1GdXR1cmVXYXJuaW5nKQoKaW1wb3J0IG1hdHBsb3RsaWIucHlwbG90IGFzIHBsdAppbXBvcnQgbnVtcHkgYXMgbnAKaW1wb3J0IHBhbmRhcyBhcyBwZAoKaW1wb3J0IHRlbnNvcmZsb3cgYXMgdGYKcHJpbnQodGYuX192ZXJzaW9uX18pCmlmIHRmLnRlc3QuaXNfZ3B1X2F2YWlsYWJsZSgpOgogIHByaW50KHRmLnRlc3QuZ3B1X2RldmljZV9uYW1lKCkpCgppbXBvcnQgc2tsZWFybgpmcm9tIHNrbGVhcm4uZmVhdHVyZV9leHRyYWN0aW9uLnRleHQgaW1wb3J0IFRmaWRmVmVjdG9yaXplcgpmcm9tIHNrbGVhcm4uZW5zZW1ibGUgaW1wb3J0IFJhbmRvbUZvcmVzdENsYXNzaWZpZXIKZnJvbSBza2xlYXJuLnBpcGVsaW5lIGltcG9ydCBtYWtlX3BpcGVsaW5lCmZyb20gc2tsZWFybi5tZXRyaWNzIGltcG9ydCBjbGFzc2lmaWNhdGlvbl9yZXBvcnQsIHJvY19hdWNfc2NvcmUKZnJvbSBza2xlYXJuLm1vZGVsX3NlbGVjdGlvbiBpbXBvcnQgdHJhaW5fdGVzdF9zcGxpdAppbXBvcnQgam9ibGliCgpwcmludChza2xlYXJuLl9fdmVyc2lvbl9fKQpgYGAKCmBgYHtweXRob24gbWtkaXJ9CiMgQ3JlYXRlIG1vZGVsIGRpciB0byBjYWNoZSBhbGwgbW9kZWxzIHRyYWluZWQgaW4gdGhlIG5vdGVib29rLgptb2RlbF9kaXIgPSAibW9kZWxzIgppZiBub3Qgb3MucGF0aC5leGlzdHMobW9kZWxfZGlyKToKICAgIG9zLm1ha2VkaXJzKG1vZGVsX2RpcikKCiMgRGlyZWN0b3J5IHRvIGNhY2hlIGRhdGFzZXQuCmhvbWUgPSBvcy5wYXRoLmV4cGFuZHVzZXIoIn4iKQpjYWNoZV9kaXIgPSBvcy5wYXRoLmpvaW4oaG9tZSwgIi5rZXJhcyIpCmBgYAoKRmlyc3QsCndlIHByZXBhcmUgdGhlIG1vdmllIHJldmlldyBkYXRhc2V0Ll5bS2VyYXMgYWxzbyBjb21lcyB3aXRoIHRoZSBkYXRhc2V0IHByZXByb2Nlc3NlZCBhcyBpbnRlZ2VyIHNlcXVlbmNlcyAoYGZyb20gdGYua2VyYXMuZGF0YXNldHMgaW1wb3J0IGltZGJgKS5dCgpgYGB7cHl0aG9uIG1heWJlX2Rvd25sb2FkX2ltZGIsIHJlc3VsdHM9ImhpZGUifQppbXBvcnQgdGVuc29yZmxvd19kYXRhc2V0cyBhcyB0ZmRzCgojIExvYWQgdGhlIGRhdGEgYXMgdGYuZGF0YS5EYXRhc2V0LgppbWRiID0gdGZkcy5sb2FkKG5hbWU9ImltZGJfcmV2aWV3cyIsIGFzX3N1cGVydmlzZWQ9VHJ1ZSwKICAgICAgICAgICAgICAgICBkYXRhX2Rpcj1vcy5wYXRoLmpvaW4oaG9tZSwgInRlbnNvcmZsb3dfZGF0YXNldHMiKSkKYGBgCgpUaGUgZGF0YXNldCBpcyBhIHBlcmZlY3RseSBiYWxhbmNlZCBkYXRhc2V0IHdpdGggNTAsMDAwIGV4YW1wbGVzLApoYWxmIGZvciBwb3NpdGl2ZSBhbmQgaGFsZiBmb3IgbmVnYXRpdmUgc2VudGltZW50LgoKYGBge3B5dGhvbiBwcmVwYXJlX2ltZGJ9CiMgRXh0cmFjdCBhbGwgdGV4dHMgYXMgbGlzdCBzaW5jZSB3ZSB3YW50IHRvIHVzZSBsaWJyYXJpZXMgb3RoZXIgdGhhbiB0ZW5zb3JmbG93IGFzIHdlbGwuCiMgQW5kIHNpbmNlIHRoaXMgaXMgYSBzbWFsbCBkYXRhc2V0LCB3ZSBkb24ndCBjYXJlIGFib3V0IG1lbW9yeSB1c2FnZS4KIyBXZSBza2lwIHRoZSB1c2Ugb2YgYSBkYXRhc2V0IGl0ZXJhdG9yLgppbWRiX3Jldmlld3NfdHJhaW4gPSBbXQppbWRiX3Jldmlld3NfdGVzdCA9IFtdCmltZGJfeV90cmFpbiA9IFtdCmltZGJfeV90ZXN0ID0gW10KZm9yIHgsIHkgaW4gaW1kYlsidHJhaW4iXS5iYXRjaCgxMjgpOgogIGltZGJfcmV2aWV3c190cmFpbi5leHRlbmQoeC5udW1weSgpKQogIGltZGJfeV90cmFpbi5leHRlbmQoeS5udW1weSgpKQpmb3IgeCwgeSBpbiBpbWRiWyJ0ZXN0Il0uYmF0Y2goMTI4KToKICBpbWRiX3Jldmlld3NfdGVzdC5leHRlbmQoeC5udW1weSgpKQogIGltZGJfeV90ZXN0LmV4dGVuZCh5Lm51bXB5KCkpCgojIFRGIHdvcmtzIG9uIGJ5dGVzLCBidXQgc29tZSBvdGhlciBwYWNrYWdlcyBtYXkgb25seSB3b3JrIG9uIGRlY29kZWQgc3RyaW5nLgppbWRiX3Jldmlld3NfdHJhaW4gPSBbYi5kZWNvZGUoInV0ZjgiKSBmb3IgYiBpbiBpbWRiX3Jldmlld3NfdHJhaW5dCmltZGJfcmV2aWV3c190ZXN0ID0gW2IuZGVjb2RlKCJ1dGY4IikgZm9yIGIgaW4gaW1kYl9yZXZpZXdzX3Rlc3RdCmltZGJfeV90cmFpbiA9IG5wLmFycmF5KGltZGJfeV90cmFpbikKaW1kYl95X3Rlc3QgPSBucC5hcnJheShpbWRiX3lfdGVzdCkKCiMgVGFrZSBvbmUgcmV2aWV3LgpwcmludChpbWRiX3Jldmlld3NfdHJhaW5bODddKQoKcHJpbnQoaW1kYl95X3RyYWluWzg3XSkgICMgTGFiZWwuIDAgYXMgbmVnYXRpdmUgYW5kIDEgYXMgcG9zaXRpdmUuCmBgYAoKV2UgdXNlIHRoZSBkYXRhIHByZXBhcmVkIGJ5IGB0ZW5zb3JmbG93LWRhdGFzZXRzYCBoZXJlIGp1c3QgdG8gc2F2ZSBzb21lIHRpbWUuCkZvciB0aG9zZSB3aG8gd2FudCB0byBwcm9jZXNzIHRoZSBkYXRhIGluIGl0cyB2ZXJ5IG9yaWdpbmFsIGZvcm1hdCAod2hlcmUgb25lIHJldmlldyBpcyBpbiBvbmUgYC50eHRgIGZpbGUpLAp0aGUgZmlsZXMgY2FuIGJlIGRvd25sb2FkZWQgYnkgdGhpcyBwaWVjZSBvZiBjb2RlOgoKYGBgcHl0aG9uCmltZGJfcmVtb3RlX3BhdGggPSAiaHR0cHM6Ly9haS5zdGFuZm9yZC5lZHUvfmFtYWFzL2RhdGEvc2VudGltZW50L2FjbEltZGJfdjEudGFyLmd6IgppbWRiX2ZuYW1lID0gb3MucGF0aC5iYXNlbmFtZShpbWRiX3JlbW90ZV9wYXRoKQppbWRiX2xvY2FsX3BhdGggPSBvcy5wYXRoLmpvaW4oY2FjaGVfZGlyLCAiZGF0YXNldHMiLCBpbWRiX2ZuYW1lKQoKaWYgbm90IG9zLnBhdGguZXhpc3RzKGltZGJfbG9jYWxfcGF0aCk6CiAgXyA9IHRmLmtlcmFzLnV0aWxzLmdldF9maWxlKGZuYW1lPWltZGJfZm5hbWUsIG9yaWdpbj1pbWRiX3JlbW90ZV9wYXRoLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBleHRyYWN0PVRydWUsIGNhY2hlX2Rpcj1jYWNoZV9kaXIpCmBgYAoKIyMjIyBFeHBsYWluIFJhbmRvbSBGb3Jlc3Qgey19CgpMZXQncyBidWlsZCBhIHJhbmRvbSBmb3Jlc3Qgd2l0aCBURi1JREYgYXMgb3VyIGZlYXR1cmUgc3BhY2UuCldlIHdpbGwgdXNlIHRoZSBwb3B1bGFyIGBzY2lraXQtbGVhcm5gIGxpYnJhcnkgZm9yIGltcGxlbWVudGF0aW9uLgpeW0l0IHdpbGwgYmUgbXVjaCBmYXN0ZXIgaWYgd2UgY2hvb3NlIGB4Z2Jvb3N0YCdzIG9yIGBsaWdodGdibWAncyBpbXBsZW1lbnRhdGlvbiBvZiByYW5kb20gZm9yZXN0LgpIb3dldmVyLCB0byBkZW1vbnN0cmF0ZSBjb21wYXRpYmlsaXR5IG9mIGBsaW1lYCB3aXRoIGBzY2lraXQtbGVhcm5gIHdlIHB1cnBvc2VseSBjaG9vc2UgdGhlIHNsb3dlciBpbXBsZW1lbnRhdGlvbiBoZXJlLl0KCmBgYHtweXRob24gdGZpZGZ9CiMgV2UgZHJvcCB3b3JkcyB0aGF0IGFyZSB0b28gZnJlcXVlbnQgb3IgdG9vIHJhcmUgaW4gdGhlIHRyYWluaW5nIGRhdGFzZXQuCmltZGJfdmVjdG9yaXplciA9IFRmaWRmVmVjdG9yaXplcihsb3dlcmNhc2U9VHJ1ZSwgbWluX2RmPTEwLCBtYXhfZGY9LjkpCmltZGJfWF90cmFpbiA9IGltZGJfdmVjdG9yaXplci5maXRfdHJhbnNmb3JtKGltZGJfcmV2aWV3c190cmFpbikKaW1kYl9YX3Rlc3QgPSBpbWRiX3ZlY3Rvcml6ZXIudHJhbnNmb3JtKGltZGJfcmV2aWV3c190ZXN0KQpwcmludChsZW4oaW1kYl92ZWN0b3JpemVyLnZvY2FidWxhcnlfKSkgICMgV2l0aG91dCBPT1YgdG9rZW4uCmBgYAoKYGBge3B5dGhvbiBpbWRiX3JmfQppbWRiX3JmX21vZGVsX2ZpbGUgPSBvcy5wYXRoLmpvaW4obW9kZWxfZGlyLCAidGV4dF9yZi5qb2JsaWIiKQoKIyBTYXZlL3JlbG9hZCB0aGUgbW9kZWwgdG8gc2F2ZSBub3RlYm9vayByZW5kZXJpbmcgdGltZS4KaWYgb3MucGF0aC5leGlzdHMoaW1kYl9yZl9tb2RlbF9maWxlKToKICBpbWRiX3JmID0gam9ibGliLmxvYWQoaW1kYl9yZl9tb2RlbF9maWxlKQplbHNlOgogIGltZGJfcmYgPSBSYW5kb21Gb3Jlc3RDbGFzc2lmaWVyKG5fZXN0aW1hdG9ycz0zMDAsIHJhbmRvbV9zdGF0ZT02NCwgbl9qb2JzPS0yKQogIF8gPSBpbWRiX3JmLmZpdChpbWRiX1hfdHJhaW4sIGltZGJfeV90cmFpbikKICBfID0gam9ibGliLmR1bXAoaW1kYl9yZiwgaW1kYl9yZl9tb2RlbF9maWxlKQoKaW1kYl9yZl9wcmVkID0gaW1kYl9yZi5wcmVkaWN0KGltZGJfWF90ZXN0KQppbWRiX3JmX3loYXQgPSBpbWRiX3JmLnByZWRpY3RfcHJvYmEoaW1kYl9YX3Rlc3QpWzosMV0KCnByaW50KGNsYXNzaWZpY2F0aW9uX3JlcG9ydChpbWRiX3lfdGVzdCwgaW1kYl9yZl9wcmVkKSkKcHJpbnQocm9jX2F1Y19zY29yZShpbWRiX3lfdGVzdCwgaW1kYl9yZl95aGF0KSkKYGBgCgpBcyBhIGJhc2VsaW5lIHdpdGhvdXQgZXh0ZW5zaXZlIHR1bmluZyAod2UgZGlkbid0IHR1bmUgYW55dGhpbmcgaW5kZWVkISksCnJhbmRvbSBmb3Jlc3Qgc2VlbXMgdG8gcGVyZm9ybSBmYWlybHkgd2VsbCBvbiB0aGlzIGRhdGFzZXQuCgpBcyBwYXJ0IG9mIHRoZSBhbGdvcml0aG0ncyBkZXNpZ24gd2UgYXJlIGFibGUgdG8gZGVyaXZlIGEgZ2xvYmFsIHZpZXcgb2YgZmVhdHVyZSBpbXBvcnRhbmNlLgpUaGlzIGlzIGJhc2VkIG9uIGhvdyBtdWNoIGVhY2ggZmVhdHVyZSBjYW4gcmVkdWNlIHRoZSBpbXB1cml0eSBkdXJpbmcgYWxsIHRyZWUgc3BsaXR0aW5ncy4KRm9yIGV4YW1wbGUsCndlIGNhbiBwbG90IHRoZSB0b3AgMjAgZmVhdHVyZXM6CgpgYGB7cHl0aG9uIGltZGJfcmZfZmVhdF9pbXB9CnNvcnRlZF92b2NhYiA9IHNvcnRlZChpbWRiX3ZlY3Rvcml6ZXIudm9jYWJ1bGFyeV8uaXRlbXMoKSwga2V5PWxhbWJkYSBrdjoga3ZbMV0pCnNvcnRlZF92b2NhYiA9IFt3IGZvciB3LCBpIGluIHNvcnRlZF92b2NhYl0KCmltZGJfcmZfZmVhdF9pbXAgPSBwZC5TZXJpZXMoaW1kYl9yZi5mZWF0dXJlX2ltcG9ydGFuY2VzXywgaW5kZXg9c29ydGVkX3ZvY2FiKS5zb3J0X3ZhbHVlcygpCmF4ID0gaW1kYl9yZl9mZWF0X2ltcC50YWlsKDIwKS5wbG90KGtpbmQ9ImJhcmgiKQpwbHQuc2hvdygpCmBgYAoKQXMgb25lIGNhbiBzZWUsCmNvbW1vbiBhZGplY3RpdmVzIGRlc2NyaWJpbmcgZ29vZCBvciBiYWQgdGhpbmdzIGdlbmVyYWxseSBoYXZlIGxhcmdlciBpbXBhY3QgaW4gdGhlIG1vZGVsLAp3aGljaCBpcyB0b3RhbGx5IGV4cGVjdGVkLgpCdXQgd2UgYWxzbyBzZWUgaW5mbHVlbnRpYWwgd29yZHMgc3VjaCBhcyBganVzdGAgYW5kIGBtaW51dGVzYCB3aGljaCBhcmUgcXVpdGUgbmV1dHJhbCBhbmQgY29udGFpbiBubyB1c2VmdWwgaW5mb3JtYXRpb24gb24gdGhlaXIgb3duLgpUaGV5IG1heSBiZSAqam9pbnRseSogaW1wb3J0YW50IGluIHRoZSBtb2RlbCBzaW5jZSBhIHRyZWUgbW9kZWwgYWxsb3dzIGludGVyYWN0aW9uIGJldHdlZW4gdmFyaWFibGVzLgpCdXQgd2Ugd29uJ3QgYmUgYWJsZSB0byBnbyBkZWVwZXIgYmV5b25kIHRoZSB1bmNvbmRpdGlvbmFsIHZpZXcgd2UgZGVyaXZlZCBhcyBhIGdsb2JhbCBmZWF0dXJlIHJhbmtpbmcuCgpJbnRlcnByZXRhdGlvbiBvZiB0aGUgaW1wdXJpdHktYmFzZWQgcmFua2luZyBtdXN0IGJlIHZlcnkgY2FyZWZ1bC4KRm9yIGV4YW1wbGUsCnJlbGF0ZWQgZmVhdHVyZXMgd2lsbCB0aGVvcmV0aWNhbGx5IGhhdmUgc2ltaWxhciBpbXBhY3QgYnV0IG9ubHkgb25lIG9mIGl0IHdpbGwgZ2FpbiBoaWdoZXIgc2NvcmUgKGFuZCBzdXBwcmVzcyB0aGUgb3RoZXIpIGluIHRoZSByYW5raW5nLgpXaGljaCBvbmUgc3RhbmRzIG91dCBpcyB0b3RhbGx5IHJhbmRvbSBkdWUgdG8gdGhlIHdheSB0cmVlIHNwbGl0dGluZyBpcyBwZXJmb3JtZWQgZHVyaW5nIHRyYWluaW5nLgoKSW4gZ2VuZXJhbCBpdCBpcyBOT1QgcmVjb21tZW5kZWQgdG8gdXNlIGltcHVyaXR5IG9yIGxvc3MtYmFzZWQgZmVhdHVyZSByYW5raW5nIHRvICppbnRlcnByZXQqIGEgdHJlZSBlbnNlbWJsZSBtb2RlbC4KU3VjaCByYW5raW5nIGluZm9ybWF0aW9uIGlzIHN0aWxsIHVzZWZ1bCB0byB1bmRlcnN0YW5kIGRpZmZlcmVudCBhc3BlY3RzIG9mIHRoZSBtb2RlbCwKYW5kIGNhbiBiZSB1c2VkIHRvIHN1YnNldCBmZWF0dXJlIHRvIGNvdW50ZXIgb3Zlci1maXR0aW5nIGlzc3VlLCBpZiBhbnkuCkJ1dCBpdCB3b24ndCBoZWxwIHJlYWxseSBleHBsYWluIHRoZSBtb2RlbCBhdCB0aGUgcHJlZGljdGlvbi1sZXZlbDogKldoeSBpcyBteSBtb2RlbCBtYWtpbmcgc3VjaCBwcmVkaWN0aW9uPyoKQW5kIHRoaXMgaXMgZXhhY3RseSB3aHkgd2UgbmVlZCBhIGV4cGxhbmF0aW9uIG1vZGVsIGluIHRoZSBmaXJzdCBwbGFjZS4KCk5vdyBtb3ZlIG9uIHRvIG1vZGVsIGV4cGxhbmF0aW9uIHdpdGggTElNRS4KV2UgcGljayB1cCBvbmUgdHJ1ZSBwb3NpdGl2ZSBhbmQgb25lIGZhbHNlIHBvc2l0aXZlIGNhc2UgbWFkZSBieSBvdXIgcmFuZG9tIGZvcmVzdCBtb2RlbCB0byBzZWUgaG93IHRoZSBleHBsYW5hdGlvbiBtb2RlbCB3aWxsIGV4cGxhaW4gZWFjaCBjYXNlLgoKYGBge3B5dGhvbiBsaW1lX2ltZGJfcmZ9CmZyb20gbGltZS5saW1lX3RleHQgaW1wb3J0IExpbWVUZXh0RXhwbGFpbmVyCgojIFdlIG5lZWQgYSBwaXBlbGluZSBzaW5jZSBMaW1lVGV4dEV4cGxhaW5lci5leHBsYWluX2luc3RhbmNlIGV4cGVjdHMgcmF3IHRleHQgaW5wdXQuCmltZGJfcmZfcGlwZSA9IG1ha2VfcGlwZWxpbmUoaW1kYl92ZWN0b3JpemVyLCBpbWRiX3JmKQppbWRiX3JmX2V4cGxhaW5lciA9IExpbWVUZXh0RXhwbGFpbmVyKGNsYXNzX25hbWVzPVsiTmVnYXRpdmUiLCAiUG9zaXRpdmUiXSwgcmFuZG9tX3N0YXRlPTY0KQoKaW1kYl9yZl90cF9pZHggPSBucC53aGVyZShucC5sb2dpY2FsX2FuZChpbWRiX3JmX3ByZWQgPT0gMSwgaW1kYl95X3Rlc3QgPT0gMSkpWzBdCmltZGJfcmZfZnBfaWR4ID0gbnAud2hlcmUobnAubG9naWNhbF9hbmQoaW1kYl9yZl9wcmVkID09IDEsIGltZGJfeV90ZXN0ID09IDApKVswXQoKIyBXZSB0YWtlIG9uZSB0cnVlIHBvc2l0aXZlIGFuZCBvbmUgZmFsc2UgcG9zaXRpdmUgZXhhbXBsZSB0byBkZW1vIGV4cGxhbmF0aW9uLgppbWRiX3JmX3RwX2V4cCA9IGltZGJfcmZfZXhwbGFpbmVyLmV4cGxhaW5faW5zdGFuY2UoCiAgaW1kYl9yZXZpZXdzX3Rlc3RbaW1kYl9yZl90cF9pZHhbMF1dLCBpbWRiX3JmX3BpcGUucHJlZGljdF9wcm9iYSwgbnVtX2ZlYXR1cmVzPTYpCmltZGJfcmZfZnBfZXhwID0gaW1kYl9yZl9leHBsYWluZXIuZXhwbGFpbl9pbnN0YW5jZSgKICBpbWRiX3Jldmlld3NfdGVzdFtpbWRiX3JmX2ZwX2lkeFswXV0sIGltZGJfcmZfcGlwZS5wcmVkaWN0X3Byb2JhLCBudW1fZmVhdHVyZXM9NikKIyBGb3IgaXB5bmIsIG9uZSBjYW4gc2ltcGx5IGNhbGwgaW1kYl90cF9leHAuc2hvd19pbl9ub3RlYm9vayh0ZXh0PVRydWUpIHRvIGVtYmVkIHRoZSBodG1sIG91dHB1dC4KCmltZGJfcmZfdHBfZXhwLnNhdmVfdG9fZmlsZSgiL3RtcC9leHBsYWluX3RleHRfcmZfdHAuaHRtbCIpCmltZGJfcmZfZnBfZXhwLnNhdmVfdG9fZmlsZSgiL3RtcC9leHBsYWluX3RleHRfcmZfZnAuaHRtbCIpCmBgYAoKIyMjIyMgQSBUcnVlIFBvc2l0aXZlIFByZWRpY3Rpb24gRXhwbGFpbmVkIHstfQoKYGBge3IsIGVjaG89RkFMU0V9CmlmICggIWZpbGUuZXhpc3RzKCJsaW1lLmpzIikgKSB7CiAgd3JpdGVfbGltZV9qcygiL3RtcC9leHBsYWluX3RleHRfcmZfdHAuaHRtbCIpCn0KaHRtbHRvb2xzOjppbmNsdWRlSFRNTChwYXJzZV9saW1lX2h0bWxfb3V0cHV0KCIvdG1wL2V4cGxhaW5fdGV4dF9yZl90cC5odG1sIikpCmBgYAoKPGJyPgoKT3VyIFJGIG1vZGVsIGRvZXNuJ3Qgc2VlbSB0byBiZSB2ZXJ5IGNvbmZpZGVudCBvbiB0aGlzIHBhcnRpY3VsYXIgcG9zaXRpdmUgZXhhbXBsZSBpbmRlZWQuClRoZXJlIGlzIG5vIGRvbWluYW50IHNpbmdsZSB3b3JkIGNhbiBkcml2ZSB0aGUgcHJlZGljdGlvbiBpbiB0aGUgY29ycmVjdCBkaXJlY3Rpb24uClRoZSBjb250cmlidXRpbmcgd29yZHMgYXJlIGFsc28gbW9zdGx5IG5ldXRyYWwgb24gdGhlaXIgb3duLgpXZSBjYW4gY29uZmlybSB0aGF0IHRoZSByZXN1bHQgb2YgdGhpcyBwcmVkaWN0aW9uIHdpbGwgYmUgdmVyeSBzZW5zaXRpdmUgYW5kIG5vdCByb2J1c3QuCkFkbWl0dGVkbHkgdGhpcyByZXZpZXcgZG9lcyBzaG93IHNvbWUgbWl4dHVyZXMgb2YgcG9zaXRpdmUgYW5kIG5lZ2F0aXZlIHZpZXdzLgoKIyMjIyMgQSBGYWxzZSBQb3NpdGl2ZSBQcmVkaWN0aW9uIEV4cGxhaW5lZCB7LX0KCk5vdyBsZXQncyBsb29rIGF0IGEgZmFsc2UgcG9zaXRpdmUgZXhhbXBsZSwKd2hlcmUgb3VyIFJGIG1vZGVsIHdyb25nbHkgbGFiZWxlZCBhcyBhIHBvc2l0aXZlIHJldmlldy4KCmBgYHtyLCBlY2hvPUZBTFNFfQpodG1sdG9vbHM6OmluY2x1ZGVIVE1MKHBhcnNlX2xpbWVfaHRtbF9vdXRwdXQoIi90bXAvZXhwbGFpbl90ZXh0X3JmX2ZwLmh0bWwiKSkKYGBgCgo8YnI+CgpJbiB0aGlzIGV4YW1wbGUgYSBzaW5nbGUgcG9zaXRpdmUgd29yZCBgZ3JlYXRgICh3cm9uZ2x5KSBkb21pbmF0ZSB0aGUgcHJlZGljdGlvbiB0b3dhcmQgYSBwb3NpdGl2ZSBzZW50aW1lbnQuCkFuZCB3ZSByZWFsaXplIHRoZSBtb2RlbCBkaWRuJ3QgcmVzcG9uc2Ugd2VsbCB0byBzb21lIG5lZ2F0aXZlIHNpZ25hbHMsCmVzcGVjaWFsbHkgZm9yIHRoZSB3b3JkIGBib3JlYC4KCklmIHdlIGV4YW1pbmUgbW9yZSBjYXNlcyB3ZSBtYXkgaGF2ZSBtb3JlIGNsdWVzIG9uIGhvdyB0aGUgbW9kZWwgbWlzLWJlaGF2ZXMsCmFuZCB3ZSBjYW4gY29tZSB1cCB3aXRoIGEgc3RyYXRlZ3kgYWNjb3JkaW5nbHkgdG8gaW1wcm92ZSBpdC4KRm9yIG5vdyB3ZSdsbCBzdG9wIGhlcmUgYW5kIHRyeSBleHBlcmltZW50aW5nIHdpdGggb3RoZXIgbGVhcm5pbmcgYWxnb3JpdGhtcyBoZXJlYWZlci4KCiMjIyMjIEJ1aWxkIExJTUUgZnJvbSBTY3JhdGNoIHstfQoKTGV0J3MgdXNlIHRoZSB0ZXh0IGRhdGEgZXhhbXBsZSBhYm92ZSB0byBidWlsZCBhIExJTUUgbW9kZWwgZnJvbSBzY3JhdGNoIHRvIGJldHRlciB1bmRlcnN0YW5kIGV2ZXJ5IGRldGFpbCBvZiB0aGUgdGVjaG5pcXVlLgoKYGBge3B5dGhvbiBsaW5tZV9mcm9tX3NjcmF0Y2h9CmZyb20gc2NpcHkuc3BhcnNlIGltcG9ydCBjc3JfbWF0cml4CmZyb20gc2tsZWFybi5saW5lYXJfbW9kZWwgaW1wb3J0IFJpZGdlLCBsYXJzX3BhdGgKZnJvbSBza2xlYXJuLm1ldHJpY3MucGFpcndpc2UgaW1wb3J0IGNvc2luZV9kaXN0YW5jZXMKCk4gPSAxMDAwICAjIE51bWJlciBvZiBsb2NhbCBzYW1wbGVzLgpLID0gMTAgICMgTnVtYmVyIG9mIGZlYXR1cmVzIHRvIHVzZWQuCmkgPSBpbWRiX3JmX3RwX2lkeFswXSAgIyBUaGUgdGFyZ2V0IGV4YW1wbGUuCnggPSBpbWRiX1hfdGVzdFtpXQpsb2NhbF9zYW1wbGVzX3ggPSBjc3JfbWF0cml4KG5wLm9uZXMoW04sIDFdKSkgKiB4ICAjIENvbnRhaW5lciBmb3IgcGVydHVyYmF0aW9uLgpwcmVzZW50X3Rva19pZCA9IHgubm9uemVybygpWzFdICAjIFByZXNlbnQgZmVhdHVyZXMuCgojIEdlbmVyYXRlIHJhbmRvbSBkZXNpZ24gbWF0cml4IGZvciB0aGUgZXhwbGFuYXRpb24gbW9kZWwuCiMgVGhpcyBpcyB1bmRlciB0aGUgaW50ZXJwcmV0YWJsZSAoYmluYXJ5KSBzcGFjZS4KbG9jYWxfc2FtcGxlc196ID0gKG5wLnJhbmRvbS51bmlmb3JtKHNpemU9KE4sIGxlbihwcmVzZW50X3Rva19pZCkpKSA+IC41KS5hc3R5cGUoaW50KQoKIyBQcmVkaWN0IGxvY2FsIHNhbXBsZXMgYnkgdGhlIG9yaWdpbmFsIG1vZGVsLgpyZW1vdmVfaW5kID0gcGQuRGF0YUZyYW1lKHppcCgqbnAud2hlcmUobG9jYWxfc2FtcGxlc196ID09IDApKSwgY29sdW1ucz1bInJpZCIsICJwb3MiXSkKZm9yIHIgaW4gcmFuZ2UoMSwgTik6CiAgIyBXZSBrZWVwIHRoZSBmaXJzdCBzYW1wbGUgYXMgdGhlIG9yaWdpbmFsIHRhcmdldCBleGFtcGxlLgogIGRmID0gcmVtb3ZlX2luZFtyZW1vdmVfaW5kLnJpZCA9PSByXQogIGlmIG5vdCBkZi5lbXB0eToKICAgIHRva19pZHNfdG9fcmVtb3ZlID0gcHJlc2VudF90b2tfaWRbZGYucG9zXQogICAgbG9jYWxfc2FtcGxlc194W3IsdG9rX2lkc190b19yZW1vdmVdID0gMApsb2NhbF9zYW1wbGVzX3guZWxpbWluYXRlX3plcm9zKCkKbG9jYWxfc2FtcGxlc195ID0gaW1kYl9yZi5wcmVkaWN0X3Byb2JhKGxvY2FsX3NhbXBsZXNfeClbOiwxXQoKIyBDYWxjdWxhdGUgcHJveGltaXR5IHdlaWdodHMgdW5kZXIgdGhlIGludGVycHJldGFibGUgc3BhY2UuCmRlZiBwaV94KHopOgogIGtlcm5lbF93aWR0aCA9IDI1CiAgZGlzdCA9IGNvc2luZV9kaXN0YW5jZXMoelswXS5yZXNoYXBlKDEsIC0xKSwgeikucmF2ZWwoKQogIHJldHVybiBucC5zcXJ0KG5wLmV4cCgtKChkaXN0ICogMTAwKSAqKiAyKSAvIGtlcm5lbF93aWR0aCAqKiAyKSkKCndlaWdodHMgPSBwaV94KGxvY2FsX3NhbXBsZXNfeikKCiMgU3Vic2V0IHRvcCBLIGZlYXR1cmVzIHdpdGggTEFSIHBhdGguCndlaWdodGVkX3ogPSAoKGxvY2FsX3NhbXBsZXNfeiAtIG5wLmF2ZXJhZ2UobG9jYWxfc2FtcGxlc196LCBheGlzPTAsIHdlaWdodHM9d2VpZ2h0cykpCiAgKiBucC5zcXJ0KHdlaWdodHNbOiwgbnAubmV3YXhpc10pKQp3ZWlnaHRlZF95ID0gKChsb2NhbF9zYW1wbGVzX3kgLSBucC5hdmVyYWdlKGxvY2FsX3NhbXBsZXNfeSwgd2VpZ2h0cz13ZWlnaHRzKSkKICAqIG5wLnNxcnQod2VpZ2h0cykpCl8sIF8sIGNvZWZzID0gbGFyc19wYXRoKHdlaWdodGVkX3osIHdlaWdodGVkX3ksIG1ldGhvZD0ibGFzc28iKQoKbm9uemVyb19jb2VmcyA9IHJhbmdlKHdlaWdodGVkX3ouc2hhcGVbMV0pCmZvciBpIGluIHJhbmdlKGxlbihjb2Vmcy5UKSAtIDEsIDAsIC0xKToKICAgIG5vbnplcm9fY29lZnMgPSBjb2Vmcy5UW2ldLm5vbnplcm8oKVswXQogICAgaWYgbGVuKG5vbnplcm9fY29lZnMpIDw9IDEwOgogICAgICAgIGJyZWFrCgojIExlYXJuIHRoZSBleHBsYW5hdGlvbiBtb2RlbC4KZXhwbGFpbmVyID0gUmlkZ2UoYWxwaGE9MSwgZml0X2ludGVyY2VwdD1UcnVlLCByYW5kb21fc3RhdGU9NjQpCl8gPSBleHBsYWluZXIuZml0KGxvY2FsX3NhbXBsZXNfels6LG5vbnplcm9fY29lZnNdLCBsb2NhbF9zYW1wbGVzX3ksIHNhbXBsZV93ZWlnaHQ9d2VpZ2h0cykKCiMgRml0bmVzcy4KIyBUaGlzIGNhbiBiZSBhIHNjb3JlIHRvIGp1ZGdlIGhvdyBnb29kIHRoZSBsb2NhbCBhcHByb3hpbWF0aW9uIGlzLgpwcmludChleHBsYWluZXIuc2NvcmUobG9jYWxfc2FtcGxlc196Wzosbm9uemVyb19jb2Vmc10sIGxvY2FsX3NhbXBsZXNfeSwgc2FtcGxlX3dlaWdodD13ZWlnaHRzKSkKCmV4cCA9IHBkLkRhdGFGcmFtZSh7CiAgInRvayI6IG5wLmFycmF5KHNvcnRlZF92b2NhYilbcHJlc2VudF90b2tfaWRbbm9uemVyb19jb2Vmc11dLAogICJpbXAiOiBleHBsYWluZXIuY29lZl8KfSkKcHJpbnQoZXhwLnNvcnRfdmFsdWVzKCJpbXAiLCBhc2NlbmRpbmc9RmFsc2UpKQpgYGAKCldlIHRyeSBhIHNtYWxsZXIgbG9jYWwgc2FtcGxlIHNpemUgaW4gb3VyIGV4ZXJjaXNlLApidXQgd2UgY2FuIGFscmVhZHkgc3VjY2Vzc2Z1bGx5IGNhbGN1bGF0ZSB2ZXJ5IGNsb3NlbHkgdGhlIGZlYXR1cmUgY29udHJpYnV0aW9uIHNjb3JlcyBhcyBpbiBgbGltZWAncyBBUEkuCgojIyMjIEV4cGxhaW4gTmV1cmFsIE5ldHdvcmtzIHstfQoKTm93IGxldCdzIHRyeSBhIHNoYWxsb3cgbmV1cmFsIG5ldHdvcmsgbW9kZWwgd2l0aCB3b3JkIGVtYmVkZGluZ3MgdHJhaW5lZCBmcm9tIHNjcmF0Y2guCldlIHVzZSBgdGVuc29yZmxvdy5rZXJhc2AgQVBJIHRvIHF1aWNrbHkgYnVpbGQgYW5kIHRyYWluIGEgbmV1cmFsIG5ldC4KV2UgYXZlcmFnZSB3b3JkIGVtYmVkZGluZ3MgYXMgdGhlIGRvY3VtZW50IGVtYmVkZGluZ3MgZm9yIGVhY2ggcmV2aWV3LAp0aGVuIGZlZWQtZm9yd2FyZCBhIFJlTFUgbGF5ZXIgYmVmb3JlIHRoZSBzaWdtb2lkIGFjdGl2YXRpb24gZm9yIGNyb3NzLWVudHJvcHkgb3B0aW1pemF0aW9uLgoKQXMgYW4gZXhlcmNpc2UsCmluc3RlYWQgb2YgcmUtdXNpbmcgdGhlIHZvY2FidWxhcnkgYnVpbHQgYnkgYFRmaWRmVmVjdG9yaXplcmAgd2l0aCBgc2Npa2l0LWxlYXJuYCwKd2Ugd2lsbCByZS10b2tlbml6ZSB0aGUgdGV4dCBkYXRhIHdpdGggYGtlcmFzLnByZXByb2Nlc3NpbmdgIG1vZHVsZS4KVGhlIGluaGVyZW50IGNvbnNpc3RlbmN5IHVuZGVyIHRoZSBLZXJhcyBmcmFtZXdvcmsgd2lsbCBhbHNvIHNpbXBsaWZ5IG91ciBsYXR0ZXIgd29ya3Mgb24gbmV0d29yayBsYXllcmluZy4KCmBgYHtweXRob24gaW1kYl9ubn0KZnJvbSB0ZW5zb3JmbG93LmtlcmFzLnByZXByb2Nlc3NpbmcudGV4dCBpbXBvcnQgVG9rZW5pemVyCmZyb20gdGVuc29yZmxvdy5rZXJhcy5wcmVwcm9jZXNzaW5nLnNlcXVlbmNlIGltcG9ydCBwYWRfc2VxdWVuY2VzCgojIEJ1aWxkIHZvY2FidWxhcnkuIFdlIHVzZSBzaW1pbGFyIHNpemUgYXMgaW4gb3VyIHByZXZpb3VzIFRmaWRmVmVjdG9yaXplci4KIyBTaW5jZSB3ZSB3aWxsIHVzZSB6ZXJvIHBhZGRpbmcsIDAgY2Fubm90IGJlIHVzZWQgYXMgT09WIGluZGV4LgojIEtlcmFzIHRva2VuaXplciBieSBkZWZhdWx0IHJlc2VydmVzIDAgYWxyZWFkeS4gT09WIHRva2VuLCBpZiB1c2VkLCB3aWxsIGJlIGluZGV4ZWQgYXQgMS4KIyBOb3RlIHRoYXQgbGVuKHRva2VuaXplci5pbmRleF93b3JkKSB3aWxsIGJlIGFsbCB2b2NhYnVsYXJ5IGluc3RlYWQgb2YgYG51bV93b3Jkc2AuCnZvY2FiX3NpemUgPSAyMDAwMSAgIyArMSBmb3IgMCBpbmRleCB1c2VkIGZvciBwYWRkaW5nLgpvb3ZfdG9rZW4gPSAiPHVuaz4iCnRva2VuaXplciA9IFRva2VuaXplcihsb3dlcj1UcnVlLCBvb3ZfdG9rZW49b292X3Rva2VuLCBudW1fd29yZHM9dm9jYWJfc2l6ZSkKdG9rZW5pemVyLmZpdF9vbl90ZXh0cyhpbWRiX3Jldmlld3NfdHJhaW4pCgojIEVuY29kZSB0ZXh0IHdpdGggcGFkZGluZyB0byBlbnN1cmUgZml4ZWQtbGVuZ3RoIGlucHV0LgpzZXFfdHJhaW4gPSB0b2tlbml6ZXIudGV4dHNfdG9fc2VxdWVuY2VzKGltZGJfcmV2aWV3c190cmFpbikKc2VxX3RyYWluX3BhZGRlZCA9IHBhZF9zZXF1ZW5jZXMoc2VxX3RyYWluLCBwYWRkaW5nPSJwb3N0IikKbWF4bGVuID0gc2VxX3RyYWluX3BhZGRlZC5zaGFwZVsxXQpzZXFfdGVzdCA9IHRva2VuaXplci50ZXh0c190b19zZXF1ZW5jZXMoaW1kYl9yZXZpZXdzX3Rlc3QpCnNlcV90ZXN0X3BhZGRlZCA9IHBhZF9zZXF1ZW5jZXMoc2VxX3Rlc3QsIHBhZGRpbmc9InBvc3QiLCBtYXhsZW49bWF4bGVuKQoKYXNzZXJ0IHRva2VuaXplci5pbmRleF93b3JkWzFdID09IG9vdl90b2tlbgphc3NlcnQgc2VxX3RyYWluX3BhZGRlZC5tYXgoKSA9PSB2b2NhYl9zaXplIC0gMQoKIyBXcmFwIEtlcmFzIFNlcXVlbnRpYWwgbW9kZWwgd2l0aCBzY2lraXQtbGVhcm4gQVBJLgojIFRoaXMgaXMgYmVjYXVzZSBMaW1lVGV4dEV4cGxhaW5lciBzZWVtcyBidWdneSB3aXRoIGEgbmF0aXZlIEtlcmFzIG1vZGVsLgpubl9tb2RlbF9maWxlID0gb3MucGF0aC5qb2luKG1vZGVsX2RpciwgInRleHRfY2xmX25uLmg1IikKCmRlZiBubl9tb2RlbF9mbigpOgogIGVtYmVkZGluZ19zaXplID0gNjQKICBtb2RlbCA9IHRmLmtlcmFzLlNlcXVlbnRpYWwoWwogICAgdGYua2VyYXMubGF5ZXJzLkVtYmVkZGluZygKICAgICAgdm9jYWJfc2l6ZSwgZW1iZWRkaW5nX3NpemUsIGlucHV0X2xlbmd0aD1tYXhsZW4sCiAgICAgIG1hc2tfemVybz1UcnVlLCBuYW1lPSJ3b3JkX2VtYmVkZGluZyIpLAogICAgdGYua2VyYXMubGF5ZXJzLkdsb2JhbEF2ZXJhZ2VQb29saW5nMUQobmFtZT0iZG9jX2VtYmVkZGluZyIpLAogICAgdGYua2VyYXMubGF5ZXJzLkRlbnNlKGVtYmVkZGluZ19zaXplIC8gMiwgYWN0aXZhdGlvbj0icmVsdSIsIG5hbWU9InJlbHUiKSwKICAgIHRmLmtlcmFzLmxheWVycy5EZW5zZSgxLCBhY3RpdmF0aW9uPSJzaWdtb2lkIiwgbmFtZT0ic2lnbW9pZCIpCiAgXSwgbmFtZT0ibm5fY2xhc3NpZmllciIpCiAgbW9kZWwuY29tcGlsZShvcHRpbWl6ZXI9ImFkYW0iLAogICAgICAgICAgICAgICAgbG9zcz0iYmluYXJ5X2Nyb3NzZW50cm9weSIsCiAgICAgICAgICAgICAgICBtZXRyaWNzPVsiYWNjdXJhY3kiXSkKICByZXR1cm4gbW9kZWwKCnByaW50KG5uX21vZGVsX2ZuKCkuc3VtbWFyeShsaW5lX2xlbmd0aD05MCkpCgppbWRiX25uID0gdGYua2VyYXMud3JhcHBlcnMuc2Npa2l0X2xlYXJuLktlcmFzQ2xhc3NpZmllcihubl9tb2RlbF9mbikKaWYgbm90IG9zLnBhdGguZXhpc3RzKG5uX21vZGVsX2ZpbGUpOgogIG1ldHJpY3MgPSBpbWRiX25uLmZpdCgKICAgIHg9c2VxX3RyYWluX3BhZGRlZCwgeT1pbWRiX3lfdHJhaW4sCiAgICBiYXRjaF9zaXplPTI1NiwgZXBvY2hzPTEwLAogICAgdmFsaWRhdGlvbl9kYXRhPShzZXFfdGVzdF9wYWRkZWQsIGltZGJfeV90ZXN0KSwKICAgIHZhbGlkYXRpb25fc3RlcHM9MjAsCiAgICBjYWxsYmFja3M9WwogICAgICB0Zi5rZXJhcy5jYWxsYmFja3MuRWFybHlTdG9wcGluZyhtb25pdG9yPSJ2YWxfbG9zcyIsIHBhdGllbmNlPTIpLAogICAgICB0Zi5rZXJhcy5jYWxsYmFja3MuTW9kZWxDaGVja3BvaW50KG5uX21vZGVsX2ZpbGUsIG1vbml0b3I9InZhbF9sb3NzIiwgc2F2ZV9iZXN0X29ubHk9VHJ1ZSkKICAgIF0sCiAgICB2ZXJib3NlPTIpCgojIFJlc3RvcmUgdGhlIG1vZGVsIHdpdGggd3JhcHBlci4KaW1kYl9ubi5tb2RlbCA9IHRmLmtlcmFzLm1vZGVscy5sb2FkX21vZGVsKG5uX21vZGVsX2ZpbGUpCmltZGJfbm4uY2xhc3Nlc18gPSBucC5hcnJheShbMCwgMV0pCmltZGJfbm5feWhhdCA9IGltZGJfbm4ucHJlZGljdF9wcm9iYShzZXFfdGVzdF9wYWRkZWQpWzosMV0KaW1kYl9ubl9wcmVkID0gKGltZGJfbm5feWhhdCA+IC41KS5hc3R5cGUoaW50KQoKcHJpbnQoY2xhc3NpZmljYXRpb25fcmVwb3J0KGltZGJfeV90ZXN0LCBpbWRiX25uX3ByZWQpKQpwcmludChyb2NfYXVjX3Njb3JlKGltZGJfeV90ZXN0LCBpbWRiX25uX3loYXQpKQpgYGAKCkJhc2VkIG9uIHRoZSB0ZXN0aW5nIEFVQyBzY29yZSwKb3VyIHNoYWxsb3cgbmV1cmFsIG5ldHdvcmsgbW9kZWwgZGlkIG91dHBlcmZvcm0gYSByYW5kb20gZm9yZXN0LgpMZXQncyBzZWUgaG93IHRoZSBleHBsYW5hdGlvbiBtb2RlbCB0ZWxsIHVzIGFib3V0IHRoZSBiZWhhdmlvciBvZiB0aGUgbmV1cmFsIG5ldHdvcmsgbW9kZWwuCgpgYGB7cHl0aG9uIGxpbWVfaW1kYl9ubn0KZGVmIG5uX3ByZWRpY3RfZm4odGV4dCk6CiAgIyBUaGlzIGlzIGZvciBza2xlYXJuIHdyYXBwZXIgb25seS4KICBzZXEgPSB0b2tlbml6ZXIudGV4dHNfdG9fc2VxdWVuY2VzKHRleHQpCiAgc2VxID0gcGFkX3NlcXVlbmNlcyhzZXEsIHBhZGRpbmc9InBvc3QiLCBtYXhsZW49bWF4bGVuKQogIHJldHVybiBpbWRiX25uLnByZWRpY3RfcHJvYmEoc2VxKQoKaW1kYl9ubl9leHBsYWluZXIgPSBMaW1lVGV4dEV4cGxhaW5lcihjbGFzc19uYW1lcz1bIk5lZ2F0aXZlIiwgIlBvc2l0aXZlIl0pCgojIEV4cGxhaW4gdGhlIHNhbWUgZXhhbXBsZXMgYXMgaW4gUkYuCmltZGJfbm5fdHBfZXhwID0gaW1kYl9ubl9leHBsYWluZXIuZXhwbGFpbl9pbnN0YW5jZSgKICBpbWRiX3Jldmlld3NfdGVzdFtpbWRiX3JmX3RwX2lkeFswXV0sIG5uX3ByZWRpY3RfZm4sIG51bV9mZWF0dXJlcz02KQppbWRiX25uX2ZwX2V4cCA9IGltZGJfbm5fZXhwbGFpbmVyLmV4cGxhaW5faW5zdGFuY2UoCiAgaW1kYl9yZXZpZXdzX3Rlc3RbaW1kYl9yZl9mcF9pZHhbMF1dLCBubl9wcmVkaWN0X2ZuLCBudW1fZmVhdHVyZXM9NikKCmltZGJfbm5fdHBfZXhwLnNhdmVfdG9fZmlsZSgiL3RtcC9leHBsYWluX3RleHRfbm5fdHAuaHRtbCIpCmltZGJfbm5fZnBfZXhwLnNhdmVfdG9fZmlsZSgiL3RtcC9leHBsYWluX3RleHRfbm5fZnAuaHRtbCIpCmBgYAoKYGBge3IsIGVjaG89RkFMU0V9Cmh0bWx0b29sczo6aW5jbHVkZUhUTUwocGFyc2VfbGltZV9odG1sX291dHB1dCgiL3RtcC9leHBsYWluX3RleHRfbm5fdHAuaHRtbCIpKQpgYGAKCjxicj4KClRoZSBhYm92ZSBpcyB0aGUgTElNRSBleHBsYW5hdGlvbiBvZiB0aGUgc2FtZSBwb3NpdGl2ZSBleGFtcGxlIHByZXZpb3VzbHkgZXhwbGFpbmVkIHdpdGggYSBSRiBtb2RlbC4KV2UgcmVhbGl6ZSB0aGF0LAp0aG91Z2ggYm90aCBtb2RlbHMgZXZlbnR1YWxseSBnaXZlIGEgcG9zaXRpdmUgcHJlZGljdGlvbiwKdGhlIG5ldXJhbCBuZXR3b3JrIG1vZGVsIGhhcyBhIHZlcnkgZGlmZmVyZW50IG9waW5pb24gb24gaG93IHRoZSBwb3NpdGl2ZSBwcmVkaWN0aW9uIGlzIGZvcm11bGF0ZWQuCkluc3RlYWQgb2YgYmVpbmcgY29uZnVzZWQgYW5kIGluZGVjaXNpdmUsCnRoZSBOTiBtb2RlbCBpcyBhY3R1YWxseSBvdmVyLWNvbmZpZGVudCBhYm91dCB0aGlzIHByZWRpY3Rpb24hClNvbWUgbmV1dHJhbCB3b3JkcyBoYXZlIGRpc3Byb3BvcnRpb25hdGUgY29udHJpYml0aW9uIHRvIHRoZSBwb3NpdGl2ZSwKcG9pbnRpbmcgb3V0IHRoZSBwb3RlbnRpYWwgZGlyZWN0aW9uIHRvIGltcHJvdmUgdGhlIG1vZGVsLgpGb3IgZXhhbXBsZSwKY2FuIGEgYmlncmFtIHRva2VuaXplciBiZSBiZXR0ZXI/CgpIb3cgYWJvdXQgdGhlIHNlY29uZCBleGFtcGxlICh3aGljaCBpcyBhIG5lZ2F0aXZlIHJldmlldyk/Ck91ciBOTiBtb2RlbCBhbHNvIG1ha2VzIGEgbWlzdGFrZSBvbiB0aGlzIG5lZ2F0aXZlIHJldmlldywKYnkgcHJlZGljdGluZyBpdCBhcyBhIHBvc2l0aXZlIG9uZS4KCmBgYHtyLCBlY2hvPUZBTFNFfQpodG1sdG9vbHM6OmluY2x1ZGVIVE1MKHBhcnNlX2xpbWVfaHRtbF9vdXRwdXQoIi90bXAvZXhwbGFpbl90ZXh0X25uX2ZwLmh0bWwiKSkKYGBgCgo8YnI+CgpXaGF0J3MgZGlmZmVyZW50IGhlcmUgaXMgdGhlIHJlYWN0aW9uIHRvIHRoZSBuZWdhdGl2ZSB3b3JkIGBib3JlYCwKd2hpY2ggaXMgbm90IHNlZW4gaW4gUkYuCgpXaXRob3V0IGEgZXhwbGFuYXRpb24gbW9kZWwsCml0IHdvbid0IGJlIGVhc3kgZm9yIHVzIHRvIGNvbXBhcmUgdHdvIG1vZGVscyBhdCB0aGlzIGxldmVsIG9mIGRldGFpbHMuCgojIyMjIEV4cGxhaW4gVHJhbnNmZXIgTGVhcm5pbmcgey19CgpPbmUgc3RlcCBmdXJ0aGVyLApsZXQncyB1c2UgcHJlLXRyYWluZWQgd29yZCBlbWJlZGRpbmdzIGZvciB0aGUgbmV1cmFsIG5ldHMgYW5kIGJ1aWxkIGFub3RoZXIgZXhwbGFuYXRpb24gbW9kZWwuCldlIHdpbGwgdXNlIFtHbG9WZV0oaHR0cHM6Ly9ubHAuc3RhbmZvcmQuZWR1L3Byb2plY3RzL2dsb3ZlLykgKEBwZW5uaW5ndG9uMjAxNGdsb3ZlKS4KV2UgdXNlIGp1c3QgdGhlIHNtYWxsZXIgR2xvVmUgbW9kZWwgc2luY2Ugb3VyIGRhdGFzZXQgaXMgcXVpdGUgc21hbGwuCgpgYGB7cHl0aG9uIG1heWJlX2Rvd25sb2FkX2dsb3ZlLCByZXN1bHRzPSJoaWRlIn0KIyBEb3dubG9hZCBHbG9WZSBwcmUtdHJhaW5lZCBlbWJlZGRpbmdzLgojIFRoZSBmaWxlIGlzIGFib3V0IDgwME1CIHNvIG1heSB0YWtlIHNvbWUgdGltZS4KZ2xvdmU2Yl9yZW1vdGVfcGF0aCA9ICJodHRwOi8vbmxwLnN0YW5mb3JkLmVkdS9kYXRhL2dsb3ZlLjZCLnppcCIKZ2xvdmU2Yl9sb2NhbF9wYXRoID0gb3MucGF0aC5qb2luKGNhY2hlX2RpciwgImRhdGFzZXRzIiwgImdsb3ZlLjZCLjUwZC50eHQiKQpnbG92ZTZiX2ZuYW1lID0gb3MucGF0aC5iYXNlbmFtZShnbG92ZTZiX3JlbW90ZV9wYXRoKQppZiBub3Qgb3MucGF0aC5leGlzdHMoZ2xvdmU2Yl9sb2NhbF9wYXRoKToKICBfID0gdGYua2VyYXMudXRpbHMuZ2V0X2ZpbGUoZm5hbWU9Z2xvdmU2Yl9mbmFtZSwgb3JpZ2luPWdsb3ZlNmJfcmVtb3RlX3BhdGgsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGV4dHJhY3Q9VHJ1ZSwgY2FjaGVfZGlyPWNhY2hlX2RpcikKCmdsb3ZlX2FsbCA9IHBkLnJlYWRfY3N2KGdsb3ZlNmJfbG9jYWxfcGF0aCwgc2VwPSIgIiwgaGVhZGVyPU5vbmUsIGluZGV4X2NvbD0wLCBxdW90aW5nPTMpCmBgYAoKSW4gYnVpbGRpbmcgdGhlIEdsb1ZlIGVtYmVkZGluZ3Mgd2UgbmVlZCB0byB0YWtlIHNwZWNpYWwgY2FyZSBhYm91dCBvdXQtb2Ytdm9jYWJ1bGFyeSB0b2tlbiBBTkQgcGFkZGluZyBpbmRleCBzaW5jZSB3ZSB3aWxsIGJlIHVzaW5nIHRoZSBLZXJhcyBBUEkuCgpgYGB7cHl0aG9uIGltZGJfdHJhbnNmZXJfbGVhcm5pbmdfdm9jYWJ9CiMgTWFwIHZvY2FidWxhcnkgdG8gcHJlLXRyYWluZWQgZW1iZWRkaW5ncy4KbWF0Y2hlZF90b2tzID0gW10KZm9yIGksIHcgaW4gdG9rZW5pemVyLmluZGV4X3dvcmQuaXRlbXMoKToKICBpZiBpIDwgdm9jYWJfc2l6ZToKICAgIGlmIHcgaW4gZ2xvdmVfYWxsLmluZGV4OgogICAgICBtYXRjaGVkX3Rva3MuYXBwZW5kKHcpCiAgICBlbHNlOgogICAgICBtYXRjaGVkX3Rva3MuYXBwZW5kKG9vdl90b2tlbikKCiMgTm90ZSB0aGF0IEdsb1ZlIHByZS10cmFpbmVkIGVtYmVkZGluZ3MgZG9lcyBub3QgaW5jbHVkZSBpdHMgb3duIE9PViB0b2tlbi4KIyBXZSB3aWxsIHVzZSBhIGdsb2JhbCBhdmVyYWdlIGVtYmVkZGluZyB0byByZXByZXNlbnQgT09WIHRva2VuLgpwcmludChsZW4oW3QgZm9yIHQgaW4gbWF0Y2hlZF90b2tzIGlmIHQgPT0gb292X3Rva2VuXSkpICAjIEhvdyBtYW55IE9PVnM/CgpnbG92ZV9hbGwubG9jW29vdl90b2tlbl0gPSBnbG92ZV9hbGwudmFsdWVzLm1lYW4oYXhpcz0wKQpnbG92ZSA9IGdsb3ZlX2FsbC5sb2NbbWF0Y2hlZF90b2tzXS52YWx1ZXMKCiMgQXBwZW5kIGR1bW15IDAtaW5kZXggdmVjdG9yIHRvIHN1cHBvcnQgcGFkZGluZy4KZ2xvdmUgPSBucC52c3RhY2soW25wLnplcm9zKCgxLCBnbG92ZS5zaGFwZVsxXSkpLCBnbG92ZV0pCnByaW50KGdsb3ZlLnNoYXBlKQpgYGAKCk5vdyBsZXQncyBidWlsZCB0aGUgbmV1cmFsIG5ldHdvcmsuCk1vc3Qgb2YgdGhlIGNvZGUgd2lsbCBiZSB0aGUgc2FtZSBhcyBiZWZvcmUsCm9ubHkgdGhlIGBFbWJlZGRpbmdgIGxheWVyIG5vdyB3ZSB3aWxsIHVzZSBhIGNvbnN0YW50IG1hdHJpeCBmb3IgaW5pdGlhbGl6YXRpb24uCldlIG1ha2UgdGhlIEdsb1ZlIGVtYmVkZGluZ3MgKnRyYWluYWJsZSogc28gaXQgd2lsbCBmdXJ0aGVyIGFkYXB0IHRvIG91ciBzcGVjaWZpYyBkYXRhc2V0LgoKYGBge3B5dGhvbiBpbWRiX3RyYW5zZmVyX2xlYXJuaW5nfQp0cl9tb2RlbF9maWxlID0gb3MucGF0aC5qb2luKG1vZGVsX2RpciwgInRleHRfY2xmX3RyLmg1IikKCmRlZiB0cl9tb2RlbF9mbigpOgogIGVtYmVkZGluZ19zaXplID0gZ2xvdmUuc2hhcGVbMV0KICBtb2RlbCA9IHRmLmtlcmFzLlNlcXVlbnRpYWwoWwogICAgdGYua2VyYXMubGF5ZXJzLkVtYmVkZGluZygKICAgICAgdm9jYWJfc2l6ZSwgZW1iZWRkaW5nX3NpemUsIGlucHV0X2xlbmd0aD1tYXhsZW4sCiAgICAgIGVtYmVkZGluZ3NfaW5pdGlhbGl6ZXI9dGYua2VyYXMuaW5pdGlhbGl6ZXJzLkNvbnN0YW50KGdsb3ZlKSwKICAgICAgdHJhaW5hYmxlPVRydWUsIG1hc2tfemVybz1UcnVlLCBuYW1lPSJnbG92ZV9lbWJlZGRpbmciKSwKICAgIHRmLmtlcmFzLmxheWVycy5HbG9iYWxBdmVyYWdlUG9vbGluZzFEKG5hbWU9ImRvY19lbWJlZGRpbmciKSwKICAgIHRmLmtlcmFzLmxheWVycy5EZW5zZShlbWJlZGRpbmdfc2l6ZSAvIDIsIGFjdGl2YXRpb249InJlbHUiLCBuYW1lPSJyZWx1IiksCiAgICB0Zi5rZXJhcy5sYXllcnMuRGVuc2UoMSwgYWN0aXZhdGlvbj0ic2lnbW9pZCIsIG5hbWU9InNpZ21vaWQiKQogIF0sIG5hbWU9InRyX2NsYXNzaWZpZXIiKQogIG1vZGVsLmNvbXBpbGUob3B0aW1pemVyPSJhZGFtIiwKICAgICAgICAgICAgICAgIGxvc3M9ImJpbmFyeV9jcm9zc2VudHJvcHkiLAogICAgICAgICAgICAgICAgbWV0cmljcz1bImFjY3VyYWN5Il0pCiAgcmV0dXJuIG1vZGVsCgpwcmludCh0cl9tb2RlbF9mbigpLnN1bW1hcnkobGluZV9sZW5ndGg9OTApKQoKaW1kYl90ciA9IHRmLmtlcmFzLndyYXBwZXJzLnNjaWtpdF9sZWFybi5LZXJhc0NsYXNzaWZpZXIodHJfbW9kZWxfZm4pCmlmIG5vdCBvcy5wYXRoLmV4aXN0cyh0cl9tb2RlbF9maWxlKToKICBpbWRiX3RyID0gdGYua2VyYXMud3JhcHBlcnMuc2Npa2l0X2xlYXJuLktlcmFzQ2xhc3NpZmllcih0cl9tb2RlbF9mbikKICBtZXRyaWNzID0gaW1kYl90ci5maXQoCiAgICB4PXNlcV90cmFpbl9wYWRkZWQsIHk9aW1kYl95X3RyYWluLAogICAgYmF0Y2hfc2l6ZT0yNTYsIGVwb2Nocz0yMCwKICAgIHZhbGlkYXRpb25fZGF0YT0oc2VxX3Rlc3RfcGFkZGVkLCBpbWRiX3lfdGVzdCksCiAgICB2YWxpZGF0aW9uX3N0ZXBzPTIwLAogICAgY2FsbGJhY2tzPVsKICAgICAgdGYua2VyYXMuY2FsbGJhY2tzLkVhcmx5U3RvcHBpbmcobW9uaXRvcj0idmFsX2xvc3MiLCBwYXRpZW5jZT0yKSwKICAgICAgdGYua2VyYXMuY2FsbGJhY2tzLk1vZGVsQ2hlY2twb2ludCh0cl9tb2RlbF9maWxlLCBtb25pdG9yPSJ2YWxfbG9zcyIsIHNhdmVfYmVzdF9vbmx5PVRydWUpCiAgICBdLAogICAgdmVyYm9zZT0yKQoKIyBSZXN0b3JlIHRoZSBtb2RlbCB3aXRoIHdyYXBwZXIuCmltZGJfdHIubW9kZWwgPSB0Zi5rZXJhcy5tb2RlbHMubG9hZF9tb2RlbCh0cl9tb2RlbF9maWxlKQppbWRiX3RyLmNsYXNzZXNfID0gbnAuYXJyYXkoWzAsIDFdKQppbWRiX3RyX3loYXQgPSBpbWRiX3RyLnByZWRpY3RfcHJvYmEoc2VxX3Rlc3RfcGFkZGVkKVs6LDFdCmltZGJfdHJfcHJlZCA9IChpbWRiX3RyX3loYXQgPiAuNSkuYXN0eXBlKGludCkKCnByaW50KGNsYXNzaWZpY2F0aW9uX3JlcG9ydChpbWRiX3lfdGVzdCwgaW1kYl90cl9wcmVkKSkKcHJpbnQocm9jX2F1Y19zY29yZShpbWRiX3lfdGVzdCwgaW1kYl90cl95aGF0KSkKYGBgCgpPdXIgTk4gbW9kZWwgd2l0aCB0cmFuc2ZlciBsZWFybmluZyBoYXMgc2ltaWxhciBBVUMgc2NvcmUgdG8gdGhlIHZhbmlsbGEgTk4uCkxldCdzIHVzZSBleHBsYW5hdGlvbiBtb2RlbGluZyB0byBzZWUgaWYgdGhlcmUgaXMgYW55IGFjdHVhbCBkaWZmZXJlbmNlLgoKYGBge3B5dGhvbiBsaW1lX2ltZGJfdHJhbnNmZXJfbGVhcm5pbmd9CmRlZiB0cl9wcmVkaWN0X2ZuKHRleHQpOgogICMgVGhpcyBpcyBmb3Igc2tsZWFybiB3cmFwcGVyIG9ubHkuCiAgc2VxID0gdG9rZW5pemVyLnRleHRzX3RvX3NlcXVlbmNlcyh0ZXh0KQogIHNlcSA9IHBhZF9zZXF1ZW5jZXMoc2VxLCBwYWRkaW5nPSJwb3N0IiwgbWF4bGVuPW1heGxlbikKICByZXR1cm4gaW1kYl90ci5wcmVkaWN0X3Byb2JhKHNlcSkKCmltZGJfdHJfZXhwbGFpbmVyID0gTGltZVRleHRFeHBsYWluZXIoY2xhc3NfbmFtZXM9WyJOZWdhdGl2ZSIsICJQb3NpdGl2ZSJdKQoKIyBFeHBsYWluIHRoZSBzYW1lIGV4YW1wbGVzIGFzIGluIFJGLgppbWRiX3RyX3RwX2V4cCA9IGltZGJfdHJfZXhwbGFpbmVyLmV4cGxhaW5faW5zdGFuY2UoCiAgaW1kYl9yZXZpZXdzX3Rlc3RbaW1kYl9yZl90cF9pZHhbMF1dLCB0cl9wcmVkaWN0X2ZuLCBudW1fZmVhdHVyZXM9NikKaW1kYl90cl9mcF9leHAgPSBpbWRiX3RyX2V4cGxhaW5lci5leHBsYWluX2luc3RhbmNlKAogIGltZGJfcmV2aWV3c190ZXN0W2ltZGJfcmZfZnBfaWR4WzBdXSwgdHJfcHJlZGljdF9mbiwgbnVtX2ZlYXR1cmVzPTYpCgppbWRiX3RyX3RwX2V4cC5zYXZlX3RvX2ZpbGUoIi90bXAvZXhwbGFpbl90ZXh0X3RyX3RwLmh0bWwiKQppbWRiX3RyX2ZwX2V4cC5zYXZlX3RvX2ZpbGUoIi90bXAvZXhwbGFpbl90ZXh0X3RyX2ZwLmh0bWwiKQpgYGAKCkZvciB0aGUgc2FtZSBwb3NpdGl2ZSByZXZpZXcsCmFnYWluIHRoZSBtb2RlbCBzaG93cyBvdmVyLWNvbmZpZGVuY2UuCkV2ZW4gdGhlIGRvbmltYW50IHdvcmRzIGFyZSB0aGUgc2FtZS4KCmBgYHtyLCBlY2hvPUZBTFNFfQpodG1sdG9vbHM6OmluY2x1ZGVIVE1MKHBhcnNlX2xpbWVfaHRtbF9vdXRwdXQoIi90bXAvZXhwbGFpbl90ZXh0X3RyX3RwLmh0bWwiKSkKYGBgCgo8YnI+CgpGb3IgdGhlIG5lZ2F0aXZlIHJldmlldywKaW50ZXJlc3RpbmdseSwKdGhlIHRyYW5zZmVyIGxlYXJuaW5nIE5OIGluZGVlZCBtYWtlcyBhIGNvcnJlY3QgcHJlZGljdGlvbiBvZiBuZWdhdGl2ZSBsYWJlbC4KVGhlIHdvcmQgYGJvcmVgIGJlY29tZXMgdGhlIG1haW4gZHJpdmluZyBmb3JjZSB0byBsb3dlciBkb3duIHRoZSBzY29yZS4KCmBgYHtyLCBlY2hvPUZBTFNFfQpodG1sdG9vbHM6OmluY2x1ZGVIVE1MKHBhcnNlX2xpbWVfaHRtbF9vdXRwdXQoIi90bXAvZXhwbGFpbl90ZXh0X3RyX2ZwLmh0bWwiKSkKYGBgCgo8YnI+CgojIyMjIEV4cGxhaW4gUmVjdXJyZW50IE5ldXJhbCBOZXRzIHstfQoKQXMgYSBmaW5hbCBleGVyY2lzZSBvbiB0ZXh0IGNsYXNzaWZpY2F0aW9uLApsZXQncyBleHBlcmltZW50IHRoZSBleHBsYW5hdGlvbiBtb2RlbGluZyB3aXRoIGEgcmVjdXJyZW50IG5ldXJhbCBuZXR3b3JrIChSTk4pClJOTiBpcyBrbm93biB0byBiZSBhYmxlIHRvIGNhcHR1cmUgc2VxdWVudGlhbCBkZXBlbmRlbmNpZXMgYmV0dGVyIHRoYW4gbmdyYW0gYmFnLW9mLXdvcmRzIGFwcHJvYWNoLgpeW05vdGUgdGhhdCwgZXZlbiBmb3IgYSBzaW5nbGUgcmVjdXJyZW50IGxheWVyLCB0cmFpbmluZyBhIFJOTiB3aWxsIGJlIHByb2hpYml0aXZlbHkgc2xvdyB3aXRob3V0IGEgR1BVLl0KCmBgYHtweXRob24gaW1kYl9ybm59CnJubl9tb2RlbF9maWxlID0gb3MucGF0aC5qb2luKG1vZGVsX2RpciwgInRleHRfY2xmX3Jubi5oNSIpCgpkZWYgcm5uX21vZGVsX2ZuKCk6CiAgZW1iZWRkaW5nX3NpemUgPSBnbG92ZS5zaGFwZVsxXQogIG1vZGVsID0gdGYua2VyYXMuU2VxdWVudGlhbChbCiAgICB0Zi5rZXJhcy5sYXllcnMuRW1iZWRkaW5nKAogICAgICB2b2NhYl9zaXplLCBlbWJlZGRpbmdfc2l6ZSwgaW5wdXRfbGVuZ3RoPW1heGxlbiwKICAgICAgZW1iZWRkaW5nc19pbml0aWFsaXplcj10Zi5rZXJhcy5pbml0aWFsaXplcnMuQ29uc3RhbnQoZ2xvdmUpLAogICAgICB0cmFpbmFibGU9VHJ1ZSwgbWFza196ZXJvPVRydWUsIG5hbWU9Imdsb3ZlX2VtYmVkZGluZyIpLAogICAgdGYua2VyYXMubGF5ZXJzLkdSVSg2NCwgZHJvcG91dD0uMiwgbmFtZT0iR1JVIiksCiAgICB0Zi5rZXJhcy5sYXllcnMuRGVuc2UoMSwgYWN0aXZhdGlvbj0ic2lnbW9pZCIsIG5hbWU9InNpZ21vaWQiKQogIF0sIG5hbWU9InJubl9jbGFzc2lmaWVyIikKICBtb2RlbC5jb21waWxlKG9wdGltaXplcj0iYWRhbSIsCiAgICAgICAgICAgICAgICBsb3NzPSJiaW5hcnlfY3Jvc3NlbnRyb3B5IiwKICAgICAgICAgICAgICAgIG1ldHJpY3M9WyJhY2N1cmFjeSJdKQogIHJldHVybiBtb2RlbAoKcHJpbnQocm5uX21vZGVsX2ZuKCkuc3VtbWFyeShsaW5lX2xlbmd0aD05MCkpCgppbWRiX3JubiA9IHRmLmtlcmFzLndyYXBwZXJzLnNjaWtpdF9sZWFybi5LZXJhc0NsYXNzaWZpZXIocm5uX21vZGVsX2ZuKQppZiBub3Qgb3MucGF0aC5leGlzdHMocm5uX21vZGVsX2ZpbGUpOgogIG1ldHJpY3MgPSBpbWRiX3Jubi5maXQoCiAgICB4PXNlcV90cmFpbl9wYWRkZWQsIHk9aW1kYl95X3RyYWluLAogICAgYmF0Y2hfc2l6ZT0zMiwgZXBvY2hzPTEwLAogICAgdmFsaWRhdGlvbl9kYXRhPShzZXFfdGVzdF9wYWRkZWQsIGltZGJfeV90ZXN0KSwKICAgIHZhbGlkYXRpb25fc3RlcHM9MjAsCiAgICBjYWxsYmFja3M9WwogICAgICB0Zi5rZXJhcy5jYWxsYmFja3MuRWFybHlTdG9wcGluZyhtb25pdG9yPSJ2YWxfbG9zcyIsIHBhdGllbmNlPTIpLAogICAgICB0Zi5rZXJhcy5jYWxsYmFja3MuTW9kZWxDaGVja3BvaW50KHJubl9tb2RlbF9maWxlLCBtb25pdG9yPSJ2YWxfbG9zcyIsIHNhdmVfYmVzdF9vbmx5PVRydWUpCiAgICBdLAogICAgdmVyYm9zZT0yKQoKIyBSZXN0b3JlIHRoZSBtb2RlbCB3aXRoIHdyYXBwZXIuCmltZGJfcm5uLm1vZGVsID0gdGYua2VyYXMubW9kZWxzLmxvYWRfbW9kZWwocm5uX21vZGVsX2ZpbGUpCmltZGJfcm5uLmNsYXNzZXNfID0gbnAuYXJyYXkoWzAsIDFdKQppbWRiX3Jubl95aGF0ID0gaW1kYl9ybm4ucHJlZGljdF9wcm9iYShzZXFfdGVzdF9wYWRkZWQpWzosMV0gICMgSW50ZXJlbmNlIG9mIFJOTiB0YWtlIHRpbWUuCmltZGJfcm5uX3ByZWQgPSAoaW1kYl9ybm5feWhhdCA+IC41KS5hc3R5cGUoaW50KQoKcHJpbnQoY2xhc3NpZmljYXRpb25fcmVwb3J0KGltZGJfeV90ZXN0LCBpbWRiX3Jubl9wcmVkKSkKcHJpbnQocm9jX2F1Y19zY29yZShpbWRiX3lfdGVzdCwgaW1kYl9ybm5feWhhdCkpCmBgYAoKUk5OIHdpdGggcHJlLXRyYWluZWQgR2xvVmUgZW1iZWRkaW5ncyBzZWVtcyB0byB3b3JrIHZlcnkgd2VsbCwKZXZlbiBmb3Igc3VjaCBhIHNtYWxsIGRhdGFzZXQuClRoYXQncyBzZWUgaG93IHRoZSBleHBsYW5hdGlvbiBjYW4gZGlmZmVyLCBhZ2FpbiwgZm9yIHRoZSBzYW1lIHR3byBleGFtcGxlczoKCmBgYHtweXRob24gbGltZV9pbWRiX3Jubn0KZGVmIHJubl9wcmVkaWN0X2ZuKHRleHQpOgogICMgVGhpcyBpcyBmb3Igc2tsZWFybiB3cmFwcGVyIG9ubHkuCiAgc2VxID0gdG9rZW5pemVyLnRleHRzX3RvX3NlcXVlbmNlcyh0ZXh0KQogIHNlcSA9IHBhZF9zZXF1ZW5jZXMoc2VxLCBwYWRkaW5nPSJwb3N0IiwgbWF4bGVuPW1heGxlbikKICByZXR1cm4gaW1kYl9ybm4ucHJlZGljdF9wcm9iYShzZXEpCgppbWRiX3Jubl9leHBsYWluZXIgPSBMaW1lVGV4dEV4cGxhaW5lcihjbGFzc19uYW1lcz1bIk5lZ2F0aXZlIiwgIlBvc2l0aXZlIl0pCgojIEV4cGxhaW4gdGhlIHNhbWUgZXhhbXBsZXMgYXMgaW4gUkYuCmltZGJfcm5uX3RwX2V4cCA9IGltZGJfcm5uX2V4cGxhaW5lci5leHBsYWluX2luc3RhbmNlKAogIGltZGJfcmV2aWV3c190ZXN0W2ltZGJfcmZfdHBfaWR4WzBdXSwgcm5uX3ByZWRpY3RfZm4sIG51bV9mZWF0dXJlcz02KQppbWRiX3Jubl9mcF9leHAgPSBpbWRiX3Jubl9leHBsYWluZXIuZXhwbGFpbl9pbnN0YW5jZSgKICBpbWRiX3Jldmlld3NfdGVzdFtpbWRiX3JmX2ZwX2lkeFswXV0sIHJubl9wcmVkaWN0X2ZuLCBudW1fZmVhdHVyZXM9NikKCmltZGJfcm5uX3RwX2V4cC5zYXZlX3RvX2ZpbGUoIi90bXAvZXhwbGFpbl90ZXh0X3Jubl90cC5odG1sIikKaW1kYl9ybm5fZnBfZXhwLnNhdmVfdG9fZmlsZSgiL3RtcC9leHBsYWluX3RleHRfcm5uX2ZwLmh0bWwiKQpgYGAKClRoZSBzYW1lIG92ZXItY29uZmlkZW5jZSBmb3IgYWxsIE5OIG1vZGVscyBvbiB0aGlzIHBhcnRpY3VsYXIgcG9zaXRpdmUgcmV2aWV3LgoKYGBge3IsIGVjaG89RkFMU0V9Cmh0bWx0b29sczo6aW5jbHVkZUhUTUwocGFyc2VfbGltZV9odG1sX291dHB1dCgiL3RtcC9leHBsYWluX3RleHRfcm5uX3RwLmh0bWwiKSkKYGBgCgo8YnI+CgpGb3IgdGhlIG5lZ2F0aXZlIHJldmlldywKUk5OIGFsc28gY29ycmVjdGx5IHByZWRpY3QgdGhlIGxhYmVsLgpUaGlzIG1heSByZWxhdGUgdG8gdGhleSBib3RoIHVzaW5nIHRoZSBwcmUtdHJhaW5lZCBlbWJlZGRpbmdzLgoKYGBge3IsIGVjaG89RkFMU0V9Cmh0bWx0b29sczo6aW5jbHVkZUhUTUwocGFyc2VfbGltZV9odG1sX291dHB1dCgiL3RtcC9leHBsYWluX3RleHRfcm5uX2ZwLmh0bWwiKSkKYGBgCgo8YnI+CgojIyMgT24gVGFidWxhciBEYXRhIENsYXNzaWZpZXJzCgpMb3RzIG9mIGRhdGEgY2FuIGJlIHJlcHJlc2VudGVkIGluIHRhYnVsYXIgZm9ybWF0LgpIZXJlIHdlIHdpbGwgdXNlIFtVQ0kgSGVhcnQgRGlzZWFzZSBkYXRhc2V0XShodHRwczovL2FyY2hpdmUuaWNzLnVjaS5lZHUvbWwvZGF0YXNldHMvSGVhcnQrRGlzZWFzZSkgZm9yIGRlbW8uClBhcnRpY3VsYXJseSwKd2UgdXNlIHRoZSBDbGV2ZWxhbmQgZGF0YXNldCB3aGljaCBpcyBjb21tb25seSB1c2VkIGluIG1hY2hpbmUgbGVhcm5pbmcgcmVzZWFyY2guCl5bVi5BLiBNZWRpY2FsIENlbnRlciwgTG9uZyBCZWFjaCBhbmQgQ2xldmVsYW5kIENsaW5pYyBGb3VuZGF0aW9uOlJvYmVydCBEZXRyYW5vLCBNLkQuLCBQaC5ELl0KCmBgYHtweXRob24gbWF5YmVfZG93bmxvYWRfdWNpaGQsIHJlc3VsdHM9ImhpZGUifQp1Y2loZF9yZW1vdGVfcGF0aCA9ICJodHRwczovL2FyY2hpdmUuaWNzLnVjaS5lZHUvbWwvbWFjaGluZS1sZWFybmluZy1kYXRhYmFzZXMvaGVhcnQtZGlzZWFzZS9wcm9jZXNzZWQuY2xldmVsYW5kLmRhdGEiCnVjaWhkX2ZuYW1lID0gb3MucGF0aC5iYXNlbmFtZSh1Y2loZF9yZW1vdGVfcGF0aCkKdWNpaGRfbG9jYWxfcGF0aCA9IG9zLnBhdGguam9pbihjYWNoZV9kaXIsICJkYXRhc2V0cyIsIHVjaWhkX2ZuYW1lKQoKaWYgbm90IG9zLnBhdGguZXhpc3RzKHVjaWhkX2xvY2FsX3BhdGgpOgogIF8gPSB0Zi5rZXJhcy51dGlscy5nZXRfZmlsZShmbmFtZT11Y2loZF9mbmFtZSwgb3JpZ2luPXVjaWhkX3JlbW90ZV9wYXRoLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBleHRyYWN0PUZhbHNlLCBjYWNoZV9kaXI9Y2FjaGVfZGlyKQpgYGAKClRoZSBkYXRhc2V0IGNvbnRhaW5zIGJvdGggbnVtZXJpY2FsIGFuZCBjYXRlZ29yaWNhbCBmZWF0dXJlcwooYWxsIGVuY29kZWQgaW4gbnVtZXJpY3MgYWxyZWFkeSwgcGxlYXNlIHJlZmVyIHRvIHRoZSBpbi1saW5lIGNvbW1lbnRzIGZvciBkb2N1bWVudGF0aW9uKS4KSXQgaXMgdGlueSBpbiBib3RoIG51bWJlciBvZiBmZWF0dXJlcyBhbmQgbnVtYmVyIG9mIGV4YW1wbGVzLgpCdXQgYXMgYSBkZW1vIGNhc2UgaXQgc2hvdWxkIHNlcnZlIHdlbGwgdGhlIHB1cnBvc2UuCgpgYGB7cHl0aG9uIHByZXByb2Nlc3NfdWNpaGR9CnVjaWhkX2F0dHIgPSBbCiAgImFnZSIsCiAgInNleCIsICAgICAgIyAwID0gZmVtYWxlIDEgPSBtYWxlCiAgImNwIiwgICAgICAgIyBjaGVzdCBwYWluIHR5cGUgMTogdHlwaWNhbCBhbmdpbmEgMjogYXR5cGljYWwgYW5naW5hIDM6IG5vbi1hbmdpbmFsIHBhaW4gNDogYXN5bXB0b21hdGljCiAgInRyZXN0YnBzIiwgIyByZXN0aW5nIGJsb29kIHByZXNzdXJlIChpbiBtbSBIZyBvbiBhZG1pc3Npb24gdG8gdGhlIGhvc3BpdGFsKQogICJjaG9sIiwgICAgICMgc2VydW0gY2hvbGVzdG9yYWwgaW4gbWcvZGwKICAiZmJzIiwgICAgICAjIChmYXN0aW5nIGJsb29kIHN1Z2FyID4gMTIwIG1nL2RsKSAoMSA9IHRydWU7IDAgPSBmYWxzZSkKICAicmVzdGVjZyIsICAjIHJlc3RpbmcgZWxlY3Ryb2NhcmRpb2dyYXBoaWMgcmVzdWx0cyAwOiBub3JtYWwgMTogaGF2aW5nIFNULVQgd2F2ZSBhYm5vcm1hbGl0eSAyOiBzaG93aW5nIHByb2JhYmxlIG9yIGRlZmluaXRlIGxlZnQgdmVudHJpY3VsYXIgaHlwZXJ0cm9waHkgYnkgRXN0ZXMnIGNyaXRlcmlhCiAgInRoYWxhY2giLCAgIyBtYXhpbXVtIGhlYXJ0IHJhdGUgYWNoaWV2ZWQKICAiZXhhbmciLCAgICAjIGV4ZXJjaXNlIGluZHVjZWQgYW5naW5hICgxID0geWVzOyAwID0gbm8pCiAgIm9sZHBlYWsiLCAgIyBTVCBkZXByZXNzaW9uIGluZHVjZWQgYnkgZXhlcmNpc2UgcmVsYXRpdmUgdG8gcmVzdAogICJzbG9wZSIsICAgICMgdGhlIHNsb3BlIG9mIHRoZSBwZWFrIGV4ZXJjaXNlIFNUIHNlZ21lbnQKICAiY2EiLCAgICAgICAjIG51bWJlciBvZiBtYWpvciB2ZXNzZWxzICgwLTMpIGNvbG9yZWQgYnkgZmxvdXJvc2NvcHkKICAidGhhbCIsICAgICAjIDMgPSBub3JtYWw7IDYgPSBmaXhlZCBkZWZlY3Q7IDcgPSByZXZlcnNhYmxlIGRlZmVjdAogICJsYWJlbCIgICAgICMgZGlhZ25vc2lzIG9mIGhlYXJ0IGRpc2Vhc2UgKGFuZ2lvZ3JhcGhpYyBkaXNlYXNlIHN0YXR1cykgMDogPCA1MCUgZGlhbWV0ZXIgbmFycm93aW5nIDEtNDogPiA1MCUgZGlhbWV0ZXIgbmFycm93aW5nCl0KdWNpaGQgPSBwZC5yZWFkX2Nzdih1Y2loZF9sb2NhbF9wYXRoLCBoZWFkZXI9Tm9uZSwgbmFtZXM9dWNpaGRfYXR0ciwgbmFfdmFsdWVzPSI/IikKY2F0ZWdvcmljYWxfYXR0ciA9IFsic2V4IiwgImNwIiwgImZicyIsICJyZXN0ZWNnIiwgImV4YW5nIiwgInRoYWwiXQpmb3IgY29sIGluIGNhdGVnb3JpY2FsX2F0dHI6CiAgdWNpaGRbY29sXSA9IHVjaWhkW2NvbF0uYXN0eXBlKCJjYXRlZ29yeSIpCgojIENsZWFuIGxhYmVsLgp1Y2loZC5sb2NbdWNpaGRbImxhYmVsIl0gPiAxLCAibGFiZWwiXSA9IDEKCnByaW50KHVjaWhkLnNoYXBlKQpwcmludCh1Y2loZC5ncm91cGJ5KCJsYWJlbCIpLnNpemUoKSkgICMgTGFiZWwgZGlzdHJpYnV0aW9uLgpwcmludCh1Y2loZC5oZWFkKCkpCmBgYAoKIyMjIyBFeHBsYWluIFJhbmRvbSBGb3Jlc3Qgey19CgpBZ2FpbiB3ZSB0cnkgdG8gZXhwbGFpbiB0cmVlIGVuc2VtYmVscy4KCmBgYHtweXRob24gdWNpaGRfcmZ9CiMgc2tsZWFybidzIGltcGxlbWVudGF0aW9uIG9mIFJGIGRvZXNuJ3QgYWxsb3cgbWlzc2luZyB2YWx1ZS4KIyBGb3IgY2F0ZWdvcmljYWwgKGFzIHN0cmluZykgd2UgY2FuIGxlYXZlIG9uZSBzcGVjaWFsIGNhdGVnb3J5IGZvciBtaXNzaW5nLAojIGJ1dCBmb3IgbnVtZXJpY2FsIHdlIG5lZWQgdG8gZG8gc29tZSBzcGVjaWFsIGVuY29kaW5nIG9yIGltcHV0YXRpb24uCnVjaWhkXzIgPSB1Y2loZC5jb3B5KCkKdWNpaGRfMi5sb2NbdWNpaGRfMlsiY2EiXS5pc25hKCksICJjYSJdID0gLTEgICMgRW5jb2RlIG1pc3NpbmcgbnVtZXJpY2FsLgoKIyBPbmUtaG90IGVuY29kZSBhbGwgY2F0ZWdvcmljYWwgZmVhdHVyZXMuCnVjaWhkXzIgPSBwZC5nZXRfZHVtbWllcyh1Y2loZF8yLCBjb2x1bW5zPWNhdGVnb3JpY2FsX2F0dHIsIGR1bW15X25hPVRydWUpCnVjaWhkX3kgPSB1Y2loZF8yLnBvcCgibGFiZWwiKQp1Y2loZF9YX3RyYWluLCB1Y2loZF9YX3Rlc3QsIHVjaWhkX3lfdHJhaW4sIHVjaWhkX3lfdGVzdCA9IHRyYWluX3Rlc3Rfc3BsaXQoCiAgdWNpaGRfMiwgdWNpaGRfeS52YWx1ZXMsIHRlc3Rfc2l6ZT0uMywgcmFuZG9tX3N0YXRlPTY0KQoKdWNpaGRfcmYgPSBSYW5kb21Gb3Jlc3RDbGFzc2lmaWVyKG5fZXN0aW1hdG9ycz0xMDAsIHJhbmRvbV9zdGF0ZT02NCkKXyA9IHVjaWhkX3JmLmZpdCh1Y2loZF9YX3RyYWluLCB1Y2loZF95X3RyYWluKQoKdWNpaGRfcmZfeWhhdCA9IHVjaWhkX3JmLnByZWRpY3RfcHJvYmEodWNpaGRfWF90ZXN0KVs6LDFdCnVjaWhkX3JmX3ByZWQgPSB1Y2loZF9yZi5wcmVkaWN0KHVjaWhkX1hfdGVzdCkKCnByaW50KGNsYXNzaWZpY2F0aW9uX3JlcG9ydCh1Y2loZF95X3Rlc3QsIHVjaWhkX3JmX3ByZWQpKQpwcmludChyb2NfYXVjX3Njb3JlKHVjaWhkX3lfdGVzdCwgdWNpaGRfcmZfeWhhdCkpCmBgYAoKQXMgb25lIGNhbiBzZWUgUkYgcGVyZm9ybXMgdmVyeSB3ZWxsIG9uIHRoaXMgZGF0YXNldC4KClRvIGV4cGxhaW4gYSBtb2RlbCB0cmFpbmVkIHdpdGggbnVtZXJpY2FsIGZlYXR1cmVzLApgbGltZWAgYnkgZGVmYXVsdCB3aWxsIGRpc2NyZXRpemUgY29udGlub3VzIHZhcmlhYmxlcyBpbnRvIHF1YW50aWxlcyBmb3IgZWFzZSBvZiBpbnRlcnByZXRhdGlvbi4KRGlzY3JldGl6YXRpb24gaXMgZG9uZSB1c2luZyBzdGF0aXN0aWNzIGRlcml2ZWQgZnJvbSB0aGUgdHJhaW5pbmcgZGF0YXNldC4KCmBgYHtweXRob24gbGltZV91Y2loZF9yZn0KZnJvbSBsaW1lLmxpbWVfdGFidWxhciBpbXBvcnQgTGltZVRhYnVsYXJFeHBsYWluZXIKCmNhdF9pbmQgPSBbaSBmb3IgaSwgY29sIGluIGVudW1lcmF0ZSh1Y2loZF8yLmNvbHVtbnMpIGlmICJfIiBpbiBjb2xdCnVjaWhkX3JmX2V4cGxhaW5lciA9IExpbWVUYWJ1bGFyRXhwbGFpbmVyKAogIHVjaWhkX1hfdHJhaW4udmFsdWVzLCBjbGFzc19uYW1lcz1bIk5lZ2F0aXZlIiwgIlBvc2l0aXZlIl0sCiAgZmVhdHVyZV9uYW1lcz11Y2loZF8yLmNvbHVtbnMsCiAgY2F0ZWdvcmljYWxfZmVhdHVyZXM9Y2F0X2luZCkKCnVjaWhkX3JmX3RwX2lkeCA9IG5wLndoZXJlKG5wLmxvZ2ljYWxfYW5kKHVjaWhkX3JmX3ByZWQgPT0gMSwgdWNpaGRfeV90ZXN0ID09IDEpKVswXQp1Y2loZF9yZl9mcF9pZHggPSBucC53aGVyZShucC5sb2dpY2FsX2FuZCh1Y2loZF9yZl9wcmVkID09IDEsIHVjaWhkX3lfdGVzdCA9PSAwKSlbMF0KCiMgV2UgdGFrZSBvbmUgdHJ1ZSBwb3NpdGl2ZSBhbmQgb25lIGZhbHNlIHBvc2l0aXZlIGZvciBleGFtcGxlcy4KdWNpaGRfcmZfdHBfZXhwID0gdWNpaGRfcmZfZXhwbGFpbmVyLmV4cGxhaW5faW5zdGFuY2UoCiAgdWNpaGRfWF90ZXN0Lmlsb2NbdWNpaGRfcmZfdHBfaWR4WzBdXSwgdWNpaGRfcmYucHJlZGljdF9wcm9iYSwgbnVtX2ZlYXR1cmVzPTQpCnVjaWhkX3JmX2ZwX2V4cCA9IHVjaWhkX3JmX2V4cGxhaW5lci5leHBsYWluX2luc3RhbmNlKAogIHVjaWhkX1hfdGVzdC5pbG9jW3VjaWhkX3JmX2ZwX2lkeFswXV0sIHVjaWhkX3JmLnByZWRpY3RfcHJvYmEsIG51bV9mZWF0dXJlcz00KQoKdWNpaGRfcmZfdHBfZXhwLnNhdmVfdG9fZmlsZSgiL3RtcC9leHBsYWluX3RhYl9yZl90cC5odG1sIikKdWNpaGRfcmZfZnBfZXhwLnNhdmVfdG9fZmlsZSgiL3RtcC9leHBsYWluX3RhYl9yZl9mcC5odG1sIikKYGBgCgpGb2xsb3dpbmcgdGhlIHNhbWUgaWRlYSBpbiBvdXIgZGlzY3Vzc2lvbiBvbiB0ZXh0IGNsYXNzaWZpZXJzLAp3ZSBjaG9vc2UgdHdvIGV4YW1wbGVzLApvbmUgdHJ1ZSBwb3NpdGl2ZSBhbmQgdGhlIG90aGVyIGZhbHNlIHBvc2l0aXZlLApmcm9tIHRoZSBSRiBwcmVkaWN0aW9ucyB0byBkZW1vbnN0cmF0ZSBleHBsYW5hdGlvbiBtb2RlbGluZy4KCiMjIyMjIEEgVHJ1ZSBQb3NpdGl2ZSBQcmVkaWN0aW9uIEV4cGxhaW5lZCB7LX0KCmBgYHtyLCBlY2hvPUZBTFNFfQpodG1sdG9vbHM6OmluY2x1ZGVIVE1MKHBhcnNlX2xpbWVfaHRtbF9vdXRwdXQoIi90bXAvZXhwbGFpbl90YWJfcmZfdHAuaHRtbCIpKQpgYGAKCjxicj4KClRoZSBleHBsYW5hdGlvbiBzdWdnZXN0cyBzZXZlcmFsIGRvbWluYW50IGZlYXR1cmVzIHRvd2FyZCB0aGUgcG9zaXRpdmUuCkZvciBjYXRlZ29yaWNhbHMgZWFjaCBjYXRlZ29yeSBzZXJ2ZXMgYXMgaW5kaXZpZHVhbCBjb250cmlidXRpb24gZm9yIGV4cGxhbmF0aW9uLgpUaGlzIGlzIGEgbmF0dXJhbCBjb25zZXF1ZW5jZSBvZiBvbmUtaG90IGVuY29kaW5nIGluIG91ciBmZWF0dXJlIHNwYWNlLgoKIyMjIyMgQSBGYWxzZSBQb3NpdGl2ZSBQcmVkaWN0aW9uIEV4cGxhaW5lZCB7LX0KCmBgYHtyLCBlY2hvPUZBTFNFfQpodG1sdG9vbHM6OmluY2x1ZGVIVE1MKHBhcnNlX2xpbWVfaHRtbF9vdXRwdXQoIi90bXAvZXhwbGFpbl90YWJfcmZfZnAuaHRtbCIpKQpgYGAKCjxicj4KCkZvciB0aGUgZmFsc2UgcG9zaXRpdmUgY2FzZSwKdGhlIG1vZGVsIGlzIGxlc3MgY29uZmlkZW50LgpUaGVyZSBhcmUgaW5kZWVkIG1vcmUgZmVhdHVyZXMgZHJpdmluZyBuZWdhdGl2ZWx5LgpCdXQgb25lIHN0cm9uZyBwb3NpdGl2ZSBjb250cmlidXRpb24gZnJvbSB0aGUgZmVhdHVyZSBgY2FgIChudW1iZXIgb2YgbWFqb3IgdmVzc2VscyBjb2xvcmVkIGJ5IGZsb3Vyb3Njb3B5KSBjYW5jZWwgb3V0IHRoZSBlbnRpcmUgbmVnYXRpdmUgZHJpdmluZyBmb3JjZXMuCgojIyMjIEV4cGxhaW4gR3JhZGllbnQgQm9vc3RpbmcgVHJlZXMgey19CgpHcmFkaWVudCBib29zdGluZyB0cmVlcyAoR0JUKSBpcyBhIHBvd2VyZnVsIG1vZGVsIGZhbWlseSBwcm92ZW4gdG8gd29yayBleGNlcHRpb25hbGx5IHdlbGwgaW4gbWFueSBkaWZmZXJlbnQgYXBwbGljYXRpb25zLgpZZXQgZHVlIHRvIGl0cyBlbnNlbWJsaW5nIG5hdHVyZSwKR0JUIGlzIGFsc28gaGFyZCB0byBpbnRyZXByZXQgaW4gZ2VuZXJhbC4KCkhlcmUgd2UgZGVtbyBgbGlnaHRnYm1gJ3MgaW1wbGVtZW50YXRpb24gb2YgR0JUIHdpdGggTElNRSBleHBsYW5hdGlvbi4KCmBgYHtweXRob24gdWNpaGRfbGdifQppbXBvcnQgbGlnaHRnYm0gYXMgbGdiCgp1Y2loZF90ciA9IGxnYi5EYXRhc2V0KHVjaWhkX1hfdHJhaW4sIGxhYmVsPXVjaWhkX3lfdHJhaW4pCnVjaWhkX3RlID0gbGdiLkRhdGFzZXQodWNpaGRfWF90ZXN0LCBsYWJlbD11Y2loZF95X3Rlc3QpCgp1Y2loZF9sZ2JfcGFyYW1zID0gewogICJsZWFybmluZ19yYXRlIjogLjAxLAogICJib29zdGluZ190eXBlIjogImdiZHQiLAogICJvYmplY3RpdmUiOiAiYmluYXJ5IiwKICAibWV0cmljIjogWyJiaW5hcnlfbG9nbG9zcyIsICJhdWMiXSwKICAibnVtX2xlYXZlcyI6IDgsCiAgIm1heF9kZXB0aCI6IDMsCiAgIm1pbl9kYXRhX3Blcl9sZWFmIjogNSwKICAidmVyYm9zZSI6IC0xLAogICJzZWVkIjogNjQKfQoKdWNpaGRfYnN0ID0gbGdiLnRyYWluKAogIHBhcmFtcz11Y2loZF9sZ2JfcGFyYW1zLAogIG51bV9ib29zdF9yb3VuZD0zMDAsIGVhcmx5X3N0b3BwaW5nX3JvdW5kcz0yMCwKICB0cmFpbl9zZXQ9dWNpaGRfdHIsIHZhbGlkX3NldHM9W3VjaWhkX3RlXSwKICB2ZXJib3NlX2V2YWw9MTApCgp1Y2loZF9sZ2JfeWhhdCA9IHVjaWhkX2JzdC5wcmVkaWN0KHVjaWhkX1hfdGVzdCkKdWNpaGRfbGdiX3ByZWQgPSAodWNpaGRfbGdiX3loYXQgPiAuNSkuYXN0eXBlKGludCkKCnByaW50KGNsYXNzaWZpY2F0aW9uX3JlcG9ydCh1Y2loZF95X3Rlc3QsIHVjaWhkX2xnYl9wcmVkKSkKcHJpbnQocm9jX2F1Y19zY29yZSh1Y2loZF95X3Rlc3QsIHVjaWhkX2xnYl95aGF0KSkKYGBgCgpJbiB0aGlzIHBhcnRpY3VsYXIgKHJhdGhlciBzbWFsbCkgZGF0YXNldCBSRiBpbmRlZWQgb3V0cGVyZm9ybXMgR0JULgpBcyBhIG1hdHRlciBvZiBmYWN0LApiYXNlZCBvbiBbZXhpc3RpbmcgYmVuY2htYXJrXShodHRwczovL2dpdGh1Yi5jb20vaW50ZXJwcmV0bWwvaW50ZXJwcmV0L3RyZWUvbWFzdGVyL2JlbmNobWFya3MpIGEgc2ltcGxlIGxvZ2lzdGljIHJlZ3Jlc3Npb24gbWF5IGhhdmUgYSBldmVuIGhpZ2hlciBzY29yZSBmb3IgdGhpcyBwcm9ibGVtLgpOZXZlcnRoZWxlc3MsCmxldCdzIG1vdmUgb24gdG8gb3VyIGV4cGxhbmF0aW9uIG1vZGVsIHdpdGggTElNRToKCmBgYHtweXRob24gbGltZV91Y2loZF9sZ2J9CmRlZiB1Y2loZF9sZ2JfcHJlZGljdF9mbih4KToKICAjIFdlIG5lZWQgdG8gb3V0cHV0IDIgY29sdW1ucyBmb3IgYmluYXJ5IHByb2IgcHJlZGljdGlvbi4KICBwID0gdWNpaGRfYnN0LnByZWRpY3QoeCkucmVzaGFwZSgtMSwgMSkKICByZXR1cm4gbnAuaHN0YWNrKCgxIC0gcCwgcCkpCgp1Y2loZF9sZ2JfZXhwbGFpbmVyID0gTGltZVRhYnVsYXJFeHBsYWluZXIoCiAgdWNpaGRfWF90cmFpbi52YWx1ZXMsIGNsYXNzX25hbWVzPVsiTmVnYXRpdmUiLCAiUG9zaXRpdmUiXSwKICBmZWF0dXJlX25hbWVzPXVjaWhkXzIuY29sdW1ucywKICBjYXRlZ29yaWNhbF9mZWF0dXJlcz1jYXRfaW5kKQoKIyBXZSB0YWtlIHRoZSBzYW1lIGV4YW1wbGVzIHByZXZpb3VzbHkgZXhwbGFpbmVkIGluIG91ciBSRiBleHBsYW5hdGlvbiBtb2RlbC4KdWNpaGRfbGdiX3RwX2V4cCA9IHVjaWhkX2xnYl9leHBsYWluZXIuZXhwbGFpbl9pbnN0YW5jZSgKICB1Y2loZF9YX3Rlc3QuaWxvY1t1Y2loZF9yZl90cF9pZHhbMF1dLCB1Y2loZF9sZ2JfcHJlZGljdF9mbiwgbnVtX2ZlYXR1cmVzPTQpCnVjaWhkX2xnYl9mcF9leHAgPSB1Y2loZF9sZ2JfZXhwbGFpbmVyLmV4cGxhaW5faW5zdGFuY2UoCiAgdWNpaGRfWF90ZXN0Lmlsb2NbdWNpaGRfcmZfZnBfaWR4WzBdXSwgdWNpaGRfbGdiX3ByZWRpY3RfZm4sIG51bV9mZWF0dXJlcz00KQoKdWNpaGRfbGdiX3RwX2V4cC5zYXZlX3RvX2ZpbGUoIi90bXAvZXhwbGFpbl90YWJfbGdiX3RwLmh0bWwiKQp1Y2loZF9sZ2JfZnBfZXhwLnNhdmVfdG9fZmlsZSgiL3RtcC9leHBsYWluX3RhYl9sZ2JfZnAuaHRtbCIpCmBgYAoKVGhlIGJlaGF2aW9yIG9mIEdCVCBsb29rcyBzaW1pbGFyIHRvIHRoYXQgb2YgUkYgaW4gdGVybXMgb2YgdGhlc2UgdHdvIGV4YW1wbGVzLgoKYGBge3IsIGVjaG89RkFMU0V9Cmh0bWx0b29sczo6aW5jbHVkZUhUTUwocGFyc2VfbGltZV9odG1sX291dHB1dCgiL3RtcC9leHBsYWluX3RhYl9sZ2JfdHAuaHRtbCIpKQpgYGAKCjxicj4KCkluIGJvdGggY2FzZSwKdGhlIHZhcmlhYmxlIGBjYWAgaGFzIGEgZG9taW5hbnQgaW1wYWN0IG9uIHRoZSBmaW5hbCBkZWNpc2lvbi4KVGhlIHR3byBtb2Rlc2wgYWxzbyBzaGFyZSB0aGUgc2FtZSBjb25mdXNpb24gYWdhaW5zdCB0aGUgbmVnYXRpdmUgZXhhbXBsZS4KCmBgYHtyLCBlY2hvPUZBTFNFfQpodG1sdG9vbHM6OmluY2x1ZGVIVE1MKHBhcnNlX2xpbWVfaHRtbF9vdXRwdXQoIi90bXAvZXhwbGFpbl90YWJfbGdiX2ZwLmh0bWwiKSkKYGBgCgo8YnI+CgojIyMjIyBPcHRpbWl6ZWQgQ2F0ZWdvcmljYWwgRW5jb2RpbmcgaW4gYGxpZ2h0Z2JtYCB7LX0KCioqVGhpcyBzZWN0aW9uIGlzIGEgZGlncmVzc2lvbiBvbiBgbGlnaHRnYm1gIHVzYWdlLioqCgpTaW5jZSBgbGltZWAncyBBUEkgcmVxdWlyZXMgdXMgdG8gcHJlcGFyZSBvdXIgZGF0YXNldCBpbiBvbmUtaG90IGVuY29kaW5nIHJlcHJlc2VudGF0aW9uLApvdXIgYGxpZ2h0Z2JtYCBjb2RlIHVzZSB0aGUgc2FtZSBkYXRhIHBpcGVsaW5lIGFzIGluIGBzY2lraXQtbGVhcm5gIHJhbmRvbSBmb3Jlc3QuCkJ1dCB0aGF0IGlzIGFjdHVhbGx5IG5vdCBvcHRpbWl6ZWQgZm9yIGBsaWdodGdibWAuClRoZSBmb2xsb3dpbmcgY29kZSBjaHVuayBzaG93Y2FzZXMgdGhlIGJlc3QgcHJhY3RpY2Ugb2YgZW5jb2RpbmcgY2F0ZWdvcmljYWxzIGluIGBsaWdodGdibWA6CldlIGRvbid0IGVuY29kZSB0aGVtIGF0IGFsbCEKCmBgYHtweXRob24gbGdiX2Jlc3RfcHJhY3RpY2V9CiMgV2UgbGVhdmUgYm90aCBtaXNzaW5ncyBhbmQgY2F0ZWdvcmljYWxzIGFzLWlzIGluIHRoZSBkYXRhc2V0Lgp1Y2loZF90cmFpbiwgdWNpaGRfdGVzdCA9IHRyYWluX3Rlc3Rfc3BsaXQodWNpaGQsIHRlc3Rfc2l6ZT0uMywgcmFuZG9tX3N0YXRlPTY0KQp1Y2loZF90ciA9IGxnYi5EYXRhc2V0KAogIHVjaWhkX3RyYWluLmRyb3AoImxhYmVsIiwgYXhpcz0xKSwgbGFiZWw9dWNpaGRfdHJhaW5bImxhYmVsIl0sCiAgY2F0ZWdvcmljYWxfZmVhdHVyZT1jYXRlZ29yaWNhbF9hdHRyLAogIGZyZWVfcmF3X2RhdGE9RmFsc2UpCnVjaWhkX3RlID0gbGdiLkRhdGFzZXQoCiAgdWNpaGRfdGVzdC5kcm9wKCJsYWJlbCIsIGF4aXM9MSksIGxhYmVsPXVjaWhkX3Rlc3RbImxhYmVsIl0sCiAgY2F0ZWdvcmljYWxfZmVhdHVyZT1jYXRlZ29yaWNhbF9hdHRyLAogIGZyZWVfcmF3X2RhdGE9RmFsc2UpCgp1Y2loZF9ic3RfMiA9IGxnYi50cmFpbigKICBwYXJhbXM9dWNpaGRfbGdiX3BhcmFtcywKICBudW1fYm9vc3Rfcm91bmQ9MzAwLCBlYXJseV9zdG9wcGluZ19yb3VuZHM9MjAsCiAgdHJhaW5fc2V0PXVjaWhkX3RyLCB2YWxpZF9zZXRzPVt1Y2loZF90ZV0sCiAgdmVyYm9zZV9ldmFsPS0xKQoKdWNpaGRfbGdiX3loYXQgPSB1Y2loZF9ic3RfMi5wcmVkaWN0KHVjaWhkX3Rlc3QuZHJvcCgibGFiZWwiLCBheGlzPTEpKQp1Y2loZF9sZ2JfcHJlZCA9ICh1Y2loZF9sZ2JfeWhhdCA+IC41KS5hc3R5cGUoaW50KQoKcHJpbnQocm9jX2F1Y19zY29yZSh1Y2loZF90ZXN0WyJsYWJlbCJdLCB1Y2loZF9sZ2JfeWhhdCkpCmBgYAoKVG8gc3VtbWFyaXplLApUaGVyZSBhcmUgdHdvIHZlcnkgc3BlY2lhbCBwcm9wZXJ0aWVzIGFib3V0IGBsaWdodGdibWAgYWxnb3JpdGhtLgpgbGlnaHRnYm1gIHRyZWF0cyBtaXNzaW5ncyBuYXRpdmVseSBhcyBhIHNwZWNpYWwgdHJlZSBzcGxpdCBwb2ludC4KVGhpcyBhbGxvd3MgdXMgdG8ga2VlcCB0aGUgb3JpZ2luYWwgbWlzc2luZyBhcyBpcyBhbmQgaW4gbWFueSBjYXNlcyBjYW4gcmVzdWx0IGluIGJldHRlciBhY2N1cmFjeSB0aGFuIGltcHV0YXRpb24uXltgeGdib29zdGAgaXMgdGhlIGZpcnN0IHRvIGludHJvZHVjZSBzdWNoIG1pc3NpbmcgdHJlYXRtZW50IGFtb25nIGFsbCB0aGUgR0JUIHBhY2thZ2UuIGBsaWdodGdibWAgZm9sbG93cy5dCgpJbiBhZGRpdGlvbiwKYGxpZ2h0Z2JtYCBlbmNvZGVzIGNhdGVnb3JpY2FsIHZhcmlhYmxlcyBpbnRlcm5hbGx5IGluIGEgbW9yZSBlZmZpY2llbnQgd2F5LgpTbyB3ZSBkb24ndCBldmVuIG5lZWQgdG8gZG8gb25lLWhvdCBlbmNvZGluZyBvbiBvdXIgb3duLgpPZiBjb3Vyc2UgaW4gdGhpcyB0aW55IGRhdGFzZXQgd2Ugd29uJ3Qgc2VlIGFueSBub3RpY2FibGUgZGlmZmVyZW5jZS4KQnV0IGZvciBsYXJnZSBhcHBsaWNhdGlvbnMgdGhlIHBlcmZvcm1hbmNlIGltcGFjdCBjYW4gYmUgaHVnZS4KV2hhdGV2ZXIsCmJ5IHNraXBwaW5nIG9uZS1ob3QgZW5jb2RpbmcgcGlwZWxpbmUgb3VyIGNvZGUgY2FuIGJlIG11Y2ggbmVhdGVyIGFzIHdlbGwuCgojIyMgT24gSW1hZ2UgQ2xhc3NpZmllcnMKCioqVE9ETzogVXNlIGEgcHJlLXRyYWluZWQgbW9kZWw/KioKCiMgU2hhcGxleSBWYWx1ZQoKU2hhcGxleSB2YWx1ZSBpcyBhIGdhbWUgdGhlb3J5IHRlcm0uCkluIGEgY29vcGVyYXRpdmUgZ2FtZSB3aXRoICRuJCBwbGF5ZXJzIChkZW5vdGVkIGFzIHNldCAkTiQgd2l0aCAkXHZlcnQgTlx2ZXJ0ID0gbiQpLApTaGFwbGV5IHZhbHVlIG9mIGEgcGxheWVyIGlzIGl0cyBjb250cmlidXRpb24gdG8gdGhlIHRvdGFsIHBheW9mZiBvZiB0aGUgZ2FtZSwKYWZ0ZXIgdGFraW5nIGludG8gYWNjb3VudCBhbGwgcG9zc2libGUgY29hbGl0aW9uIGFtb25nIHBsYXllcnMuCgojIyBBIENvb3BlcmF0aXZlIEdhbWUKCldoZW4gZWFjaCBwbGF5ZXIgaW4gYSBjb29wZXJhdGl2ZSBnYW1lIG1heSBjb250cmlidXRlIGRpZmZlcmVudGx5IHRvIHRoZSBwYXlvZmYsClNoYXBsZXkgdmFsdWUgaXMgYSB3YXkgdG8gZGV0ZXJtaW5lIGhvdyBpbXBvcnRhbnQgZWFjaCBwbGF5ZXIgaXMgdG8gdGhlIGZpbmFsIG91dGNvbWUuCgpXZSBkZWZpbmUgYSBmdW5jdGlvbiAkXG51KFMpJCB3aGVyZSAkUyQgaXMgYSBzZXQgb2YgJG0kIHBsYXllcnMgKCRcdmVydCBTXHZlcnQgPSBtJCksCnRoZSB2YWx1ZSBvZiAkXG51JCBpcyB0aGUgZXhwZWN0ZWQgcGF5b2ZmIGZyb20gYWxsIG1lbWJlcnMgaW4gJFMkIGFzIGEgY29hbGl0aW9uLgpTaGFwbGV5IHZhbHVlIHN1Z2dlc3RzIHRoYXQgdGhlIGNvbnRyaWJ1dGlvbiBvZiBhbiBpbmRpdmlkdWFsIG1lbWJlciAkaiQgaXMgY2FsY3VsYXRlZCBhcyB0aGUgZm9sbG93aW5nIGZvcm11bGE6CgokJApcdmFycGhpX2ooXG51KSA9ClxzdW0gX3tTXHN1YnNldGVxIE5cc2V0bWludXMgXHtqXH19ClxnYW1tYShTLCBOKQpcY2RvdApcYmlnZ1sgXG51KFNfe2orfSkgLSBcbnUoU197ai19KSBcYmlnZ10sCiQkCgp3aGVyZQoKJCQKXGdhbW1hKFMsIE4pID0gXGZyYWMge20hKG4gLSBtIC0gMSkhfXtuIX0KJCQKCmlzIHRoZSBwZXJtdXRhdGlvbiBwcm9wb3J0aW9uYWwgd2VpZ2h0LAphbmQgJE5cc2V0bWludXMgXHtqXH0kIGlzIHRoZSBlbnRpcmUgcGxheWVyIHNldCBleGNlcHQgcGxheWVyICRqJCwKJFNfe2orfSQgZGVub3RlcyBhIGNvYWxpdGlvbiB3aXRoIHBsYXllciAkaiQsCiRTX3tqLX0kIGRlbm90ZXMgYSBjb2FsaXRpb24gd2l0aG91dCBwbGF5ZXIgJGokLgoKU28gZXNzZW50aWFsbHkgU2hhcGxleSB2YWx1ZSBvZiBwbGF5ZXIgJGokIGlzIHRoZSAqd2VpZ2h0ZWQgYXZlcmFnZSBwYXlvZmYgZGlmZmVyZW5jZSogYmV0d2VlbiBhbGwgcG9zc2libGUgdGVhbSB3b3JrIGNvbXBvc2l0aW9ucyAoY29hbGl0aW9ucykgd2l0aCBhbmQgd2l0aG91dCAkaiQuCgojIyBNTCBNb2RlbHMgQXMgQ29vcGVyYXRpdmUgR2FtZXMKCkBsaXBvdmV0c2t5MjAwMWFuYWx5c2lzIHBvc3R1bGF0ZSBhIG1hY2hpbmUgbGVhcm5pbmcgbW9kZWwgaW5mZXJlbmNlIHRhc2sgYXMgc3VjaCBhIGNvb3BlcmF0aXZlIGdhbWUuCkVhY2ggZmVhdHVyZSB2YWx1ZSBpcyBub3cgYSBwbGF5ZXIsCnRoZSBwYXlvZmYgaXMgdGhlIGRpZmZlcmVuY2UgYmV0d2VlbiB0aGUgZmluYWwgcHJlZGljdGVkIHZhbHVlIGFuZCB0aGUgYXZlcmFnZSBwcmVkaWN0aW9uIHZhbHVlLgpJbiB0aGlzIHdheSwKU2hhcGxleSB2YWx1ZSBvZiBhIGZlYXR1cmUgY2FuIHdlbGwgcmVwcmVzZW50IGl0cyBpbXBvcnRhbmNlIGluIHRoZSBjb250cmlidXRpb24gdG8gdGhlIHByZWRpY3Rpb24uCkJhc2VkIG9uIFNoYXBlbHkgdmFsdWVzIG9mIGFsbCBmZWF0dXJlcywKd2UgYXJlIGFibGUgdG8gZXhwbGFpbiBhIGJsYWNrYm94IHByZWRpY3Rpb24uCl5bSW4gdGhlaXIgb3JpZ2luYWwgd29yayBTaGFwbGV5IHZhbHVlIGlzIHVzZWQgdG8gZXhwbGFpbiBhIGxhcmdlIHNjYWxlIHJlZ3Jlc3Npb24gcHJvYmxlbS4KQnV0IHRoZSBjb25jcGV0IGNhbiBlYXNpbHkgZXh0ZW5kIHRvIGFueSBtYWNoaW5lIGxlYXJuaW5nIG1vZGVsIG5vdCBsaW1pdGVkIHRvIGEgcmVncmVzc2lvbiBtb2RlbC5dCgpOb3cgdGhlIHJlYWwgcXVlc3Rpb24gaXMgaG93IGNhbiB3ZSBpbXBsZW1lbnQgdGhlIGNoYXJhY3RlcmlzdGljIGZ1bmN0aW9uICRcbnUoUykkIGluIG9yZGVyIHRvIGNhbGN1bGF0ZSB0aGUgcGF5b2ZmIGRpZmZlcmVuY2UgYmV0d2VlbiBhbnkgdHdvIGZlYXR1cmUgY29hbGl0aW9ucz8KVGhlcmUgYXJlIG1hbnkgZGlmZmVyZW50IGltcGxlbWVudGF0aW9ucyBvbiB0aGlzLgpIZXJlIHdlIGJyaWVmbHkgZGlzY3VzcyBzb21lIG9mIHRoZW0uCgojIyMgT2J0YWluICRcbnUoUykkIHdpdGggTW9kZWwgUmUtVHJhaW5pbmcKCiRcbnUkIGluIHRoaXMgY2FzZSBpcyBleGFjdGx5IHRoZSBtb2RlbCBwcmVkaWN0aW9uIGZ1bmN0aW9uLgpPbmUgb2J2aW91cyB3YXkgb2YgY2FsY3VsYXRpbmcgdGhlIHRlcm0gJFxudShTX3tqK30pIC0gXG51KFNfe2otfSkkIGZvciBhIGdpdmVuIGZlYXR1cmUgY29hbGl0aW9uIGlzIGhlbmNlIHRvIHRyYWluIHR3byBzZXBhcmF0ZSBtb2RlbHMsCm9uZSB3aXRoIG9ubHkgdGhlIGNvYWxpdGlvbiBhcyBmZWF0dXJlIHNldHMgKCRTX3tqLX0kKSBhbmQgdGhlIG90aGVyIHdpdGggdGhlIGNvYWxpdGlvbiBwbHVzIHRoZSBmZWF0dXJlICRqJCAoJFNfe2orfSQpLgpUaG91Z2ggdXNpbmcgc3VjaCBtZXRob2Qgd2UgY2FuIG9idGFpbiB0aGUgKmV4YWN0KiBTaGFwbGV5IHZhbHVlLApvYnZpb3VzbHkgdGhpcyB3aWxsIGJlIGluZmVhc2libGUgZm9yIGxhcmdlIGFwcGxpY2F0aW9ucy4KCiMjIyBPYnRhaW4gJFxudShTKSQgd2l0aCBSZXBsYWNlbWVudCBTYW1wbGluZwoKVG8gYnlwYXNzIHRoZSBuZWVkIGZvciByZS10cmFpbmluZyB3aXRoIGFuIGFwcHJveGltYXRpb24gc29sdXRpb24sCmFub3RoZXIgd2F5IHRvIG9idGFpbiAkXG51KFMpJCBpcyB0byBtYXJnaW5hbGl6ZSB0aGUgZWZmZWN0IG9mIGFsbCB0aGUgZmVhdHVyZXMgbm90IHByZXNlbnQgaW4gdGhlIGNvYWxpdGlvbiBzZXQgJFMkLgpUaGlzIGlzIGRvbmUgYnkgcHJlZGljdCB0aGUgc2FtZSB0YXJnZXQgaW5zdGFuY2UsCmJ1dCB3aXRoIGl0cyBub24tY29hbGl0aW9uIGZlYXR1cmUgdmFsdWVzIHJlcGxhY2VkIHdpdGggcmFuZG9tIGRyYXdzIGZyb20gdGhlIGRhdGEuCgpUaGlzIGlzIGJldHRlciBpbGx1c3RyYXRlZCB3aXRoIGFuIGV4YW1wbGUuCkxldCdzIHRha2UgdGhlIGZpcnN0IHJvdyBvZiB0aGUgVUNJIEhlYXJ0IERpc2Vhc2UgZGF0YSB3ZSBleHBsb3JlZCBqdXN0IGJlZm9yZToKCmBgYHtweXRob24gc2hhcGxleV9leGFtcGxlfQpwcmludCh1Y2loZC5pbG9jWzBdKQpgYGAKClRvIG1ha2UgdGhlIGV4YW1wbGUgbmVhdCwKYXNzdW1pbmcgd2UgYXJlIG9ubHkgbG9va2luZyBhdCBhIG1vZGVsIHRyYWluZWQgb24gdGhlIGZvbGxvd2luZyBmb3VyIGF0dHJpYnV0ZXM6CmBhZ2VgLCBgc2V4YCwgYGNwYCwgYGNhYC4KT3VyIGdvYWwgbm93IGlzIHRvIGV4cGxhaW4gdGhhdCBtb2RlbCBwcmVkaWN0aW9uIGdpdmVuIHRoZXNlIDQgZmVhdHVyZSB2YWx1ZXMuCldlIGlsbHVzdHJhdGUgdGhlIGlkZWEgdXNpbmcgdGhlIGZvbGxvd2luZyBwbG90IChhc3N1bWluZyBgYWdlYCBpcyB0aGUgZmVhdHVyZSB0byBjYWxjdWxhdGUgU2hhcGxleSB2YWx1ZSk6CgpgYGB7ciBzaGFwbGV5X2NvYWxpdGlvbl92aXosIGVjaG89RkFMU0V9CmcgPC0gLjAxCmQgPC0gZGF0YS5mcmFtZSgKICB4MT1jKDEsIDIsIDIsIDMsIDIsIDMsIDQsIDIsIDQsIDIsIDQsIDIsIDMsIDIpICsgZywKICB4Mj1jKDIsIDUsIDMsIDUsIDMsIDQsIDUsIDQsIDUsIDQsIDUsIDMsIDUsIDUpIC0gZywKICB5MT1jKDEsIDcsIDYsIDYsIDUsIDUsIDUsIDQsIDQsIDMsIDMsIDIsIDIsIDEpICsgZyoyLAogIHkyPWMoOCwgOCwgNywgNywgNiwgNiwgNiwgNSwgNSwgNCwgNCwgMywgMywgMikgLSBnKjIsCiAgcj1jKCJGaXhlZCAoT24pXG4gb3JcbiBSYW5kb21pemVkIChPZmYpIiwKICAgICAgIlJhbmRvbWl6ZWQiLAogICAgICAiRml4ZWQiLCAiUmFuZG9taXplZCIsCiAgICAgICJSYW5kb21pemVkIiwgIkZpeGVkIiwgIlJhbmRvbWl6ZWQiLAogICAgICAiUmFuZG9taXplZCIsICJGaXhlZCIsCiAgICAgICJGaXhlZCIsICJSYW5kb21pemVkIiwKICAgICAgIlJhbmRvbWl6ZWQiLCAiRml4ZWQiLAogICAgICAiRml4ZWQiKQopCmdncGxvdChkKSArCiAgZ2VvbV9yZWN0KG1hcHBpbmc9YWVzKHhtaW49eDEsIHhtYXg9eDIsIHltaW49eTEsIHltYXg9eTIsIGZpbGw9ciksIGFscGhhPTAuNSkgKwogIGdlb21fdGV4dChhZXMoeD14MSsoeDIteDEpLzIsIHk9eTErKHkyLXkxKS8yLCBsYWJlbD1yKSwgc2l6ZT00KSArCiAgbGFicyh4PSJGZWF0dXJlIFZhbHVlIChUYXJnZXQgRXhhbXBsZSkiLCB5PSJQb3NzaWJsZSBDb2FsaXRpb25zIikgKwogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbj0ibm9uZSIsCiAgICAgICAgYXhpcy50aWNrcy54PWVsZW1lbnRfYmxhbmsoKSwKICAgICAgICBwYW5lbC5iYWNrZ3JvdW5kPWVsZW1lbnRfYmxhbmsoKSkgKwogIHNjYWxlX3hfY29udGludW91cygKICAgIHBvc2l0aW9uPSJ0b3AiLAogICAgYnJlYWtzPTE6NCArIC41LAogICAgbGFiZWxzPWMoImFnZT02MyIsICJzZXg9MSIsICJjcD0xIiwgImNhPTAiKSwKICAgIGV4cGFuZD1jKDAsIDApKSArCiAgc2NhbGVfeV9jb250aW51b3VzKAogICAgYnJlYWtzPTE6NyArIC41LAogICAgbGFiZWxzPTc6MSwKICAgIGV4cGFuZD1jKDAsIDApKQpgYGAKCkVhY2ggcm93IGluIHRoZSBwbG90IHJlcHJlc2VudHMgYSBwb3NzaWJsZSBjb2FsaXRpb24gY2FzZS4KRm9yIGV4YW1wbGU6ClJvdyAxIGluZGljYXRlcyB0aGVyZSBpcyBubyBjb2FsaXRpb24gYXQgYWxsLgpSb3cgNyBpbmRpY2F0ZXMgYSBjb2FsaXRpb24gb2YgdGhlIHJlc3QgMyBmZWF0dXJlcy4KSW4gZWFjaCBjYXNlLApmZWF0dXJlcyBub3QgaW5jbHVkZWQgaW4gdGhlIGNvYWxpdGlvbiBhcmUgcmFuZG9taXplZCBieSByZXBsYWNpbmcgdGhlbSB3aXRoIGEgcmFuZG9tIGRyYXcgZnJvbSB0aGUgZGF0YXNldC4KRm9yIGVhY2ggY29hbGl0aW9uIGNhc2UgaWYgd2UgZ2VuZXJhdGUgZW5vdWdoIHN1Y2ggKGFydGlmaWNpYWwpIGluc3RhbmNlcyBhbmQgYXZlcmFnZSB0aGUgcmVzdWx0LAp3ZSBtYXJnaW5hbGl6ZSB0aGVpciBlZmZlY3RzIG9uIHRoZSBtb2RlbCwKYXMgaWYgdGhleSBhcmUgbm90IGluY2x1ZGVkIGluIHRoZSBtb2RlbCBpbiB0aGUgZmlyc3QgcGxhY2UuCl5bU3VjaCBzYW1wbGluZyBzY2hlbWUgYWN0dWFsbHkgYXNzdW1lcyBpbmRlcGVuZGVuY2UgYW1vbmcgZmVhdHVyZXMsCndoaWNoIGlzIGluIGdlbmVyYWwgbm90IHRydWUuCk1vcmUgc29waGlzdGljYXRlZCBzYW1wbGluZyBhcHByb2FjaCBjYW4gYmUgYXBwbGllZCwKYnV0IHRoYXQgd2lsbCBiZSBvdXQgb2Ygb3VyIHNjb3BlIGluIHRoaXMgbm90ZWJvb2suXQoKRm9yIHRoZSB0YXJnZXQgZmVhdHVyZSB0byBjYWxjdWxhdGUgU2hhcGxleSB2YWx1ZSwKd2UgYWxzbyB1c2UgdGhlIHNhbWUgcmFuZG9taXphdGlvbiB0ZWNobmlxdWUgdG8gaW5kaWNhdGUgd2hldGhlciBpdCBpcyBpbiBvciBvdXQgb2YgdGhlIGNvYWxpdGlvbi4KTm93ICRTX3tqK30kIGNhbiBiZSBvYnRhaW5lZCBieSBhdmVyYWdpbmcgbW9kZWwgcHJlZGljdGlvbnMgb3ZlciBhbGwgcmFuZG9taXplZCBzYW1wbGVzIHdpdGggZmVhdHVyZSBgamAgaW5jbHVkZWQgaW4gdGhlIGNvYWxpdGlvbiBhbmQgYWxsIHRoZSBvdGhlciBmZWF0dXJlcyByZXBsYWNlZCBieSByYW5kb20gZHJhd3MuCkFuZCBzaW1pbGFybHkgZm9yICRTX3tqLX0kIHdpdGggb25seSBvbmUgdHdlYWs6CmZlYXR1cmUgJGokIG5vdyBpcyBhbHNvIHJhbmRvbWl6ZWQuCkZvciBlYWNoIHBvc3NpYmxlIGNvYWxpdGlvbiAoY29ycmVzcG9uZGluZyB0byBlYWNoIHJvdyBpbiB0aGUgcGxvdCkgd2UgbmVlZCB0byBkbyB0aGlzIG1hcmdpbmFsaXphdGlvbiBjb21wdXRhdGlvbi4KQWZ0ZXIgdHJhdmVyc2UgdGhyb3VnaCBhbGwgcG9zc2libGUgY29hbGl0aW9ucywKd2UgdGFrZSB0aGUgd2VpZ2h0ZWQgYXZlcmFnZSBvZiBhbGwgcHJlZGljdGlvbiBkaWZmZXJlbmNlcyB0byBhcnJpdmUgYXQgdGhlIGVzdGltYXRlZCBTaGFwbGV5IHZhbHVlLAp3aXRoIHRoZSB3ZWlnaHRpbmcgZnVuY3Rpb24gJFxnYW1tYShTLCBOKSQuCgpFdmVuIHRob3VnaCB0aGlzIGFwcHJvYWNoIGVsaW1pbmF0ZXMgdGhlIG5lZWQgdG8gcmUtdHJhaW4gYSBwYWlyIG9mIG1vZGVscyBmb3IgZXZlcnkgY29hbGl0aW9uLApudW1iZXIgb2YgcG9zc2libGUgY29hbGl0aW9ucyBzdGlsbCBncm93IGV4cG9uZW50aWFsbHkgd2l0aCB0b3RhbCBudW1iZXIgb2YgZmVhdHVyZXMuCkFzIGEgcmVzdWx0LAppbiByZWFsaXR5LAppdCBpcyBzdGlsbCBub3QgZmVhc2libGUgZm9yIGFueSBsYXJnZSBhcHBsaWNhdGlvbi4KCiMjIyBPYnRhaW4gJFxudShTKSQgd2l0aCBNb250ZSBDYXJsbyB7I2NvYWxpdGlvbl9zYW1wbGluZ30KClRvIGZ1cnRoZXIgcmVkdWNlIHRoZSBjb21wdXRpbmcgdGltZSwKYW5vdGhlciBhcHByb2FjaCBpcyB0byB1c2UgTW9udGUgQ2FybG8gc2FtcGxlcyB0byBhcHByb3hpbWF0ZSBjb21iaW5hdGlvbnMgb2YgY29hbGl0aW9ucy4KU28gZXNzZW50aWFsbHkgdGhlIHRhc2sgd2lsbCBiZSBzaW1wbGlmaWVkIHRvIHNvbHZlOgoKJCQKXGhhdFx2YXJwaGlfaihcbnUpID0KXGZyYWN7MX17Un0gXHN1bSBfe3IgPSAxfV5SClxiaWdnWyBcbnUoU197ait9XnIpIC0gXG51KFNfe2otfV5yKSBcYmlnZ10sCiQkCgp3aGVyZSAkUiQgaXMgbnVtYmVyIG9mIE1vbnRlIENhcmxvIHNhbXBsZXMsCiRTX3tqK31eciQgaXMgYSBmZWF0dXJlIHNldCB3aXRoIGEgcmFuZG9tIG51bWJlciBvZiBmZWF0dXJlcyB3aG9zZSB2YWx1ZSBiZWluZyByZXBsYWNlZCB3aXRoIHJhbmRvbSBkcmF3cyBmcm9tIHRoZSBkYXRhLApidXQgZml4aW5nIGZlYXR1cmUgJGokLgokU197ai19XnIkIGlzIGFsbW9zdCB0aGUgc2FtZSBleGNlcHQgZm9yIGFsc28gcmFuZG9taXppbmcgZmVhdHVyZSAkaiQuCgpUaGlzIGFwcHJveGltYXRpb24gbWFrZXMgU2hhcGxleSB2YWx1ZSBhcHByb2FjaCBmZWFzaWJsZSBub3cgZXZlbiBmb3IgbGFyZ2UgYXBwbGljYXRpb25zLgoKIyMgRnJvbSBTaGFwbGV5IHRvIFNIQVAKCkluIHRoZW9yeSBhIFNoYXBsZXkgdmFsdWUgY29uc2lkZXJzIGFsbCBwb3NzaWJsZSBmZWF0dXJlIGludGVyYWN0aW9ucyBhbmQgaXMgZ2xvYmFsIGZvciB0aGUgdW5kZXJseWluZyBtYWNoaW5lIGxlYXJuaW5nIG1vZGVsLgpUaGlzIGlzIHZlcnkgZGlmZmVyZW50IGZyb20gTElNRSB3aGVyZSB0aGUgZXhwbGFuYXRpb24gaXMgb25seSBsb2NhbCB0byB0aGUgb3JpZ2luYWwgbW9kZWwuCkNvbnNlcXVlbnRseSBTaGFwbGV5IGV4cGxhbmF0aW9ucyBoYXZlIGJldHRlciBwcm9wZXJ0aWVzIHRoYW4gTElNRS4KV2UgY2FuIGludGVycHJldCBpbmRpdmlkdWFsIHByZWRpY3Rpb24sCm9yIHdlIGNhbiBjYWxjdWxhdGUgU2hhcGxleSB2YWx1ZXMgZm9yIGFsbCBleGFtcGxlcyBhbmQgYWdncmVnYXRlIHRoZSBlZmZlY3QgdG8gcHJvZmlsZSBnbG9iYWwgZmVhdHVyZSBpbXBvcnRhbmNlLgpBZ2FpbiB0aGUgb3JpZ2luYWwgbW9kZWwgY2FuIGJlIGFueSBibGFja2JveCBhbGdvcml0aG0uCkFsbCB3ZSBuZWVkIGlzIHRoZSBhY2Nlc3MgdG8gaXRzIHByZWRpY3Rpb24gaW50ZXJmYWNlIEFORCBhbHNvIHRoZSBvcmlnaW5hbCB0cmFpbmluZyBkYXRhc2V0IGluIG9yZGVyIHRvIGRvIHRoZSBzYW1wbGluZyBhcHByb3hpbWF0aW9uLgoKV2Ugd2lsbCBza2lwIHRoZSBoYW5kcy1vbiBleGVyY2lzZSBvbiBTaGFwbGV5IHZhbHVlIGFwcHJvYWNoLApqdW1waW5nIGRpcmVjdGx5IHRvIGEgbW9yZSBnZW5lcmFsIGFuZCBhbHNvIGNvbXB1dGF0aW9uYWxseSBlZmZpY2llbnQgYXBwcm9hY2g6IFNIQVAKQnV0IHRoZSBkaXNjdXNzaW9uIGhlcmUgd2lsbCBkZWZpbml0ZWx5IGhlbHAgdXMgdW5kZXJzdGFuZCBTSEFQIHNpbmNlIHRoZXkgYXJlIGNsb3NlbHkgcmVsYXRlZCB0byBlYWNoIG90aGVyLgoKIyBTSEFQCgpATklQUzIwMTdfNzA2MiBwcm9wb3NlIFNIQVAgKCoqU0hhcGxleSBBZGRpdGl2ZSBleFBsYW5hdGlvbnMqKiksCnlldCBhbm90aGVyIGFkZGl0aXZlIGZlYXR1cmUgYXR0cmlidXRpb24gbWV0aG9kIGZvciBtb2RlbCBleHBsYWluYWJpbGl0eS4KQXMgaXRzIG5hbWUgc3VnZ2VzdHMsClNIQVAgaXMgYmFzZWQgdXAgdG9wIG9mIFNoYXBsZXkgdmFsdWUuCkl0IGlzIGluZGVlZCBhIG1vcmUgZ2VuZXJhbCBhcHByb2FjaCB1bmlmeWluZyBib3RoIExJTUUgYW5kIFNoYXBsZXkgdmFsdWUgKGFuZCBtb3JlKS4KT2YgY291cnNlIFNIQVAgaXMgYWxzbyBtb2RlbC1hZ25vc3RpYy4KSW4gdGhlb3J5IGl0IGNhbiBiZSBhcHBsaWVkIHRvICphbnkqIG1hY2hpbmUgbGVhcm5pbmcgbW9kZWwsCmJ1dCBgc2hhcGAgY29tZXMgd2l0aCBhIGN1c3RvbWl6ZWQgZmFzdCBpbXBsZW1lbnRhdGlvbiBwYXJ0aWN1bGFybHkgZm9yIGdyYWRpZW50IGJvb3N0aW5nIHRyZWVzIChHQlQpLgpJdCBzdXBwb3J0cyBBUElzIG9mIHdlbGwta25vd24gb3BlbiBzb3VyY2UgR0JUIGxpYnJhcmllcyBzdWNoIGFzCltgeGdib29zdGBdKGh0dHBzOi8vZ2l0aHViLmNvbS9kbWxjL3hnYm9vc3QpLApbYGxpZ2h0Z2JtYF0oaHR0cHM6Ly9naXRodWIuY29tL21pY3Jvc29mdC9MaWdodEdCTSksCmFuZCBbYGNhdGJvb3N0YF0oaHR0cHM6Ly9naXRodWIuY29tL2NhdGJvb3N0L2NhdGJvb3N0KS4KCmBzaGFwYCBhbHNvIGNvbWVzIHdpdGggbW9yZSB2aXN1YWxpemF0aW9uIG1ldGhvZHMgZm9yIGZlYXR1cmUgaW52ZXN0aWdhdGlvbiwKZXNwZWNpYWxseSBmb3IgZmVhdHVyZSBpbnRlcmFjdGlvbiBleHBsb3JhdGlvbi4KQmVmb3JlIHdlIHByb2NlZWQgdG8gdGhlIGhhbmRzLW9uIHNlY3Rpb24sCmxldCdzIGJyaWVmbHkgZXhwbG9yZSB0aGUgbWV0aG9kb2xvZ3kgZmlyc3QuCgojIyBFc3RpbWF0ZSBTaGFwbGV5IFZhbHVlIHdpdGggYSBMaW5lYXIgTW9kZWwKClJlbWVtYmVyIHRoYXQgdGhlIGFkZGl0aXZlIGZlYXR1cmUgYXR0cmlidXRpb24gbWV0aG9kIGZvciBhbiBleHBsYW5hdGlvbiBtb2RlbCAkZyhcY2RvdCkkIGhhcyB0aGUgZm9sbHdvaW5nIGZvcm06CgokJApnKHpccHJpbWUpID0gXHBoaV8wICsgXHN1bV97aiA9IDF9Xm4gXHBoaV9qIHpfalxwcmltZS4KJCQKCkxJTUUgZXN0aW1hdGVzIHRoZSBjb250cmlidXRpb24gZmFjdG9yICRccGhpX2okIHVzaW5nIGEgbG9jYWwgcmVndWxhcml6ZWQgcmVncmVzc2lvbiBtb2RlbCwKd2l0aCBhIHdlaWdodGluZyBmdW5jdGlvbiBtZWFzdXJpbmcgcHJveGltaXR5IGJldHdlZW4gdGhlIHNhbXBsZWQgYmluYXJpemVkIGZlYXR1cmVzICR6XHByaW1lJCBhbmQgdGhlIHRhcmdldCBiaW5hcml6ZWQgZmVhdHVyZSAkeFxwcmltZSQuClNIQVAsIGluc3RlYWQsIHBvc3R1bGF0ZXMgJFxwaGlfaiQgYXMgdGhlIFNoYXBsZXkgdmFsdWUgb2YgZmVhdHVyZSAkaiQuCkl0IHR1cm5zIG91dCB0aGF0IGlmIHdlIGNvcnJlY3RseSBzZXR1cCB0aGUgd2VpZ2h0aW5nIGZ1bmN0aW9uIGluIGEgbGluZWFyIG1vZGVsLAp3ZSBjYW4gb2J0YWluIHRoZSBTaGFwbGV5IHZhbHVlIHVzaW5nIGEgd2VpZ2h0ZWQgcmVncmVzc2lvbiB3aXRoIHJhbmRvbWl6ZWQgY29hbGl0aW9uIHNhbXBsaW5nIChqdXN0IGFzIHdoYXQgd2UgZGlzY3Vzc2VkIFtoZXJlXSgjY29hbGl0aW9uX3NhbXBsaW5nKSkuCgpUaGUgd2VpZ2h0aW5nIGZ1bmN0aW9uIHByb3ZlbiB0byBiZSBhYmxlIHRvIHJlY292ZXIgU2hhcGxleSB2YWx1ZSBpczoKCiQkClxwaV97eFxwcmltZX0oelxwcmltZSkgPQpcZnJhY3ttIC0gMX0Ke1xiaW5vbXttfXtcdmVydFwgelxwcmltZVx2ZXJ0fVx2ZXJ0IHpccHJpbWVcdmVydChtIC0gXHZlcnQgelxwcmltZVx2ZXJ0KX0sCiQkCgp3aGVyZSAkbSQgaXMgdGhlIG1heGltdW0gY29hbGl0aW9uIHNpemUgcG9zc2libGUgZm9yIHRoZSBnaXZlbiBleGFtcGxlICR6JCwKYW5kICRcdmVydCB6XHByaW1lXHZlcnQkIGlzIHRoZSBzaXplIG9mIGN1cnJlbnQgY29hbGl0aW9uLgoKTm93IHdlIG9wdGltaXplIHRoZSBsb3NzOgoKJCQKTG9zcyA9IFxzdW1fe3osIHpccHJpbWV9IFxwaV97eFxwcmltZX0oelxwcmltZSkgXGNkb3QgXGJpZyhmKHopIC0gZyh6XHByaW1lKVxiaWcpXjIuCiQkCgooVGhpcyBvYmplY3RpdmUgZnVuY3Rpb24gaXMgaW4gZ2VuZXJhbCB0aGUgc2FtZSBhcyBpbiBMSU1FLikKCkluIGBzaGFwYCwKdGhlIGBLZXJuZWxFeHBsYWluZXJgIGltcGxlbWVudHMgdGhlIHdlaWdodGVkIGxpbmVhciBtb2RlbCB0byBlc3RpbWF0ZSBTaGFwbGV5IHZhbHVlLApieSB0aGUgZm9sbG93aW5nIHByb2NlZHVyZToKCjEuIENvbnN0cnVjdCBhIG51bWJlciBvZiByYW5kb20gY29hbGl0aW9uIHZlY3RvcnMgdG8gc3Vic2V0IHRoZSBmZWF0dXJlcyBvZiB0YXJnZXQgZXhhbXBsZQoyLiBUcmFuc29mb3JtIGVhY2ggc2FtcGxlIGJhY2sgdG8gdGhlIG9yaWdpbmFsIGZlYXR1cmUgc3BhY2UKICBhLiBGb3IgZWFjaCBjb2FsaXRpb24sIGRvIHJhbmRvbSBzYW1wbGUgcmVwbGFjZW1lbnQgdG8gaW1wdXRlIGZlYXR1cmVzIHRoYXQgYXJlIHJhbmRvbWx5IG1hcmtlZCBhcyBhYnNlbnQKICBiLiBGZWVkIHRoZSB0cmFuc2Zvcm1lZCBmZWF0dXJlcyBpbnRvIHRoZSBvcmlnaW5hbCBibGFja2JveCBtb2RlbCB0byBvYnRhaW4gcHJlZGljdGlvbnMgYXMgbGFiZWxzCjMuIEZpdCBhbGwgY29hbGl0aW9uIHZlY3RvcnMgd2l0aCBhIHdlaWdodGVkIGxpbmVhciByZWdyZXNzaW9uCgpTdGVwIDIgaXMgdG8gbWFyZ2luYWxpemUgb3V0IHRoZSBpbXBhY3Qgb2YgYWJzZW50IGZlYXR1cmVzLAp3aGljaCB3ZSd2ZSBkaXNjdXNzZWQgYWxyZWFkeSBpbiB0aGUgU2hhcGxleSB2YWx1ZSBzZWN0aW9uLgoKIyMjIFNIQVAgS2VybmVsIHYucy4gTElNRQoKVGhpcyBhcHByb2FjaCBjbG9zZWx5IGNvbm5lY3RzIExJTUUgd2l0aCBTaGFwbGV5IHZhbHVlIGFwcHJvYWNoLgpXZSBzdW1tYXJpemUgdGhlIG1ham9yIGRpZmZlcmVuY2UgYmV0d2VlbiBTSEFQIGFuZCBMSU1FLgoKKipXZWlnaHRpbmcgRnVuY3Rpb24qKgoKSW4gTElNRSB0aGUgY2hvaWNlIG9mIHRoZSB3ZWlnaHRpbmcgZnVuY3Rpb24gJFxwaV94XHByaW1lJCBpcyBoZXVyaXN0aWMsCndoaWxlIGluIFNIQVAgaXQgaXMgYmFzZWQgb24gYSB0aGVvcnkgdG8gcmVjb3ZlciB0aGUgU2hhcGxleSB2YWx1ZS4KCioqUmVndWxhcml6YXRpb24qKgoKQmFzZWQgb24gdGhlIHRoZW9yeSwKU0hBUCB1c2VzIGEgbGluZWFyIHJlZ3Jlc3Npb24gd2l0aG91dCByZWd1bGFyaXphdGlvbiBpbiBvcmRlciB0byBmYWl0aGZ1bGx5IHJlY292ZXIgU2hhcGxleSB2YWx1ZS4KSW4gTElNRSBpbnN0ZWFkIGEgTEFTU08gcGF0aCBpcyB1c2VkIHRvIHByZWZlciBhIHNwYXJzZSBzb2x1dGlvbi4KCiMjIyBUaGUgSW50dWl0aW9uCgpXZSAocHVycG9zZWx5KSBza2lwIHRoZSBkaXNjdXNzaW9uIG9uIHByb3ZpbmcgdGhlIGFib3ZlIHdlaWdodGVkIGxpbmVhciBtb2RlbCBjYW4gcmVjb3ZlciBTaGFwbGV5IHZhbHVlLgpGb3IgcmVhZGVycyB3aG8gYXJlIGludGVyZXN0ZWQgcGxlYXNlIHJlZmVyIHRvIHRoZSBzdXBwbGVtZW50YXJ5IG1hdGVyaWFsIHRvIHRoZSBvcmlnaW5hbCBwYXBlci4KSGVyZSBpbnN0ZWFkIHdlIGRpc2N1c3MgdGhlIGludHVpdGlvbiBiZWhpbmQgdGhlIHNjZW5lLgoKSWYgd2UgbG9vayBhdCB0aGUgZGVmaW5pdGlvbiBvZiBTaGFwbGV5IHZhbHVlOgoKJCQKXHZhcnBoaV9qKFxudSkgPQpcc3VtIF97U1xzdWJzZXRlcSBOXHNldG1pbnVzIFx7alx9fQpcZnJhYyB7bSEobiAtIG0gLSAxKSF9e24hfQpcYmlnZ1sgXG51KFNfe2orfSkgLSBcbnUoU197ai19KSBcYmlnZ10sCiQkCgp3ZSByZWFsaXplIHRoYXQgd2UgYXJlIG1lYXN1cmluZyBhbiBleHBlY3RlZCB2YWx1ZSBvZiBkaWZmZXJlbmNlLgpUbyBlc3RpbWF0ZSBzdWNoIGRpZmZlcmVuY2Ugb2YgYSBiaW5hcnkgdmFyaWFibGUgd2Uga25vdyB0aGF0IHdlIGNhbiBzaW1wbHkgaW5jbHVkZSBhIGR1bW15IHZhcmlhYmxlIGluIG91ciBkZXNpZ24gbWF0cml4IGZvciBhIHJlZ3Jlc3Npb24gcHJvYmxlbS4KTm93IHRoZSBvbmx5IHRhc2sgbGVmdCBpcyB0byBzcGVjaWZ5IGEgY29ycmVjdCB3ZWlnaHRpbmcgc2NoZW1lIHN1Y2ggdGhhdCB0aGUgZXhwZWN0YXRpb24gd2lsbCB0YWtlIGludG8gY29uc2lkZXJhdGlvbiBob3cgZmVhdHVyZSBjb2FsaXRpb24gY2FuIGJlIGZvcm1lZC4KU0hBUCBpcyBub3ZlbCBzaW5jZSBpdCBkZXJpdmVzIHRoZSBjb3JyZWN0IHdlaWdodGluZyBzY2hlbWUgc3VjaCB0aGF0IHRoZSByZWdyZXNzaW9uIGNvZWZmaWNpZW50cyBhcmUgZXhhY3RseSBTaGFwbGV5IHZhbHVlLgoKIyMgRXN0aW1hdGUgU2hhcGxleSBWYWx1ZSB3aXRoIFRyZWUgRW5zZW1ibGVzCgpBIGxpbWl0YXRpb24gb24gdGhlIGFib3ZlIGxpbmVhciByZWdyZXNzaW9uIGFwcHJvYWNoIGlzIHRoZSBhc3N1bXB0aW9uIG9mIGZlYXR1cmUgaW5kZXBlbmRlbmNlLgpJdCBpcyBhIG5hdHVyYWwgY29uc2VxdWVuY2Ugd2hlbiB3ZSBkbyBjb2FsaXRpb24gc2FtcGxpbmcgYnkgbWFyZ2luYWxpemluZyBvdXQgdGhlIGFic2VudCBmZWF0dXJlcyBpbiBvcmRlciB0byBjb25zdHJ1Y3QgcmVncmVzc2lvbiBzYW1wbGVzIHRvIGVzdGltYXRlIHRoZSBTaGFwbGV5IHZhbHVlLgoKQGx1bmRiZXJnMjAxOGNvbnNpc3RlbnQgZXh0ZW5kIHRoZSB3b3JrIGluIGBzaGFwYCwKaW50cm9kdWNlIGBUcmVlRXhwbGFpbmVyYCB0byByZWxlYXNlIHN1Y2ggY29uc3RyYWludCB3aXRoIGEgbW9kZWwtc3BlY2lmaWMgYWxnb3JpdGhtIHRvIGNhbGN1bGF0ZSBTaGFwbGV5IHZhbHVlLgpUaGlzIGNhbiBiZSBkb25lIGR1ZSB0byB0aGUgbmF0dXJlIG9mIHRyZWUgYWxnb3JpdGhtLgpUaGUgbWFyZ2luYWxpemF0aW9uIHBoYXNlIGNhbiB0YWtlIGludG8gYWNjb3VudCBmZWF0dXJlIGRlcGVuZGVuY3kgYnkgZXhhbWluZSB0aGUgdHJlZSBzdHJ1Y3R1cmUgYW5kIG9ubHkgYXZlcmFnZSBvdmVyIHRlcm1pbmFsIG5vZGVzIGNvbmRpdGlvbmFsIG9uIGFueSBnaXZlbiBub2RlLApiYXNlZCBvbiB0aGUgY29hbGl0aW9uIHNldC4KCkluIGBUcmVlRXhwbGFpbmVyYCB0aGUgZXhhY3QgU2hhcGxleSB2YWx1ZSBpcyBkaXJlY3RseSBjYWxjdWxhdGVkIHVzaW5nIHRyZWUgbm9kZXMgaW5zdGVhZCBvZiBhIGxpbmVhciBtb2RlbC4KT25lIGFkZGVkLW9uIGJlbmVmaXQgaXMgdG8gZXh0ZW5kIHRoZSBTaGFwbGV5IHZhbHVlIHRvIFNoYXBsZXkgaW50ZXJhY3Rpb24gdmFsdWUgc28gZXNzZW50aWFsbHkgYSBmZWF0dXJlIGF0dHJpYnV0aW9uIHZlY3RvciB3aWxsIGJlIGdlbmVyYWxpemVkIHRvIGEgZmVhdHVyZSBhdHRyaWJ1dGlvbiBtYXRyaXgsCndpdGggdGhlIGludGVyYWN0aW9uIHdpdGggYWxsIG90aGVyIGZlYXR1cmVzLgoKTGV0J3Mgc2tpcCB0aGUgZGV0YWlscyBhbmQgbW92ZSBvbiB0byB0aGUgYWN0dWFsIGhhbmRzLW9uIGV4YW1wbGVzIQoKIyMgSGFuZHMtb24gRXhwbGFuYXRpb24gRGVtbwoKIyMjIE9uIFRleHQgQ2xhc3NpZmllcnMKCiMjIyMgRXhwbGFpbiBSYW5kb20gRm9yZXN0IHstfQoKYHNoYXAuVHJlZUV4cGxhaW5lcmAgcGVyZm9ybWFuY2UgaXMgYmFkIGZvciBgc2Npa2l0LWxlYXJuYCdzIGBSYW5kb21Gb3Jlc3RDbGFzc2lmaWVyYC4KXltJdCBpcyBhbHNvIGJ1Z2d5IGluZGVlZC4gV2l0aCB0aGUgbmV3ZXN0IHZlcnNpb24gd2Ugc3RpbGwgZW5jb3VudGVyIHNwYXJzZSBpbnB1dCBlcnJvciBhbmQgYWRkaXRpdml0eSBjaGVjayBmYWlsdXJlLl0KVG8gZnVsbHkgZXhwbG9yZSB0aGUgY2FwYWJpbGl0eSB3ZSB3aWxsIHNraXAgdGhlIFJGIGFuZCBtb3ZlIG9uIHRvIGEgR0JUIG1vZGVsLgoKIyMjIyBFeHBsYWluIEdyYWRpZW50IEJvb3N0aW5nIFRyZWVzIHstfQoKSW4gdGhlIHByZXZpb3VzIHNlY3Rpb24gd2UgZGlkbid0IHRyYWluIGEgR0JUIGZvciB0aGUgdGV4dCBjbGFzc2lmaWNhdGlvbiBwcm9ibGVtLgpTbyBsZXQncyBxdWlja2x5IGJ1aWxkIG9uZSBzdWNoIG1vZGVsIGZpcnN0ICh3aXRoIHRoZSBzYW1lIFRGLUlERiB2ZWN0b3JpemF0aW9uIGFzIHdlIGRpZCBmb3IgdGhlIFJGIG1vZGVsKS4KCmBgYHtweXRob24gaW1kYl9sZ2J9CiMgbGlnaHRnYm0gZG9lcyBub3QgYWxsb3cgdXRmLTggZW5jb2RlZCBmZWF0dXJlIG5hbWVzLgojIFNpbmNlIGltcG9ydGFudCB0b2tlbnMgYXJlIG1vc3QgbGlrZWx5IGFzY2lpLWNvbXBhdGlibGUgZm9yIG91ciBkYXRhc2V0LAojIHdlIHNpbXBseSBzdHJpcCBub24tYXNjaWkgYXMgYSB3b3JrYXJvdW5kIGZvciB0aGlzIGV4ZXJjaXNlLgpkZWYgcmVtb3ZlX25vbl9hc2NpaShzKToKICByZXR1cm4gIiIuam9pbihbaSBpZiBvcmQoaSkgPCAxMjggZWxzZSAiXyIgZm9yIGkgaW4gc10pCgpzb3J0ZWRfdm9jYWJfYXNjaWkgPSBbcmVtb3ZlX25vbl9hc2NpaSh2KSBmb3IgdiBpbiBzb3J0ZWRfdm9jYWJdCgppbWRiX1hfdHIgPSBsZ2IuRGF0YXNldChpbWRiX1hfdHJhaW4sIGxhYmVsPWltZGJfeV90cmFpbiwgZmVhdHVyZV9uYW1lPXNvcnRlZF92b2NhYl9hc2NpaSkKaW1kYl9YX3RlID0gbGdiLkRhdGFzZXQoaW1kYl9YX3Rlc3QsIGxhYmVsPWltZGJfeV90ZXN0LCBmZWF0dXJlX25hbWU9c29ydGVkX3ZvY2FiX2FzY2lpKQoKaW1kYl9sZ2JfcGFyYW1zID0gewogICJsZWFybmluZ19yYXRlIjogLjA1LAogICJib29zdGluZ190eXBlIjogImdiZHQiLAogICJvYmplY3RpdmUiOiAiYmluYXJ5IiwKICAibWV0cmljIjogWyJiaW5hcnlfbG9nbG9zcyIsICJhdWMiXSwKICAibnVtX2xlYXZlcyI6IDE2LAogICJtYXhfZGVwdGgiOiA0LAogICJtaW5fZGF0YV9wZXJfbGVhZiI6IDIwLAogICJ2ZXJib3NlIjogLTEKfQoKaW1kYl9sZ2JfbW9kZWxfZmlsZSA9IG9zLnBhdGguam9pbihtb2RlbF9kaXIsICJ0ZXh0X2NsZl9sZ2IudHh0IikKCiMgU2F2ZS9yZWxvYWQgbW9kZWwgdG8gc2F2ZSBub3RlYm9vayByZW5kZXJpbmcgdGltZS4KaWYgb3MucGF0aC5leGlzdHMoaW1kYl9sZ2JfbW9kZWxfZmlsZSk6CiAgIyBQYXJhbWV0ZXJzIGFyZSBub3QgbG9hZGVkIGJhY2s/IChXaGljaCBjYXVzZSB0aGUgc3Vic2VxdWVudCBjYWxsIHRvIHNoYXBfdmFsdWVzIGZhaWwuKQogICMgaHR0cHM6Ly9naXRodWIuY29tL21pY3Jvc29mdC9MaWdodEdCTS9pc3N1ZXMvMjYxMwogICMgQXMgYSB3b3JrYXJvdW5kIHdlIHBhc3MgdGhlIHNhbWUgcGFyYW1ldGVycyB0byByZS1jb25zdHJ1Y3QgdGhlIG1vZGVsLgogIGltZGJfYnN0ID0gbGdiLkJvb3N0ZXIobW9kZWxfZmlsZT1pbWRiX2xnYl9tb2RlbF9maWxlLCBwYXJhbXM9aW1kYl9sZ2JfcGFyYW1zKQplbHNlOgogIGltZGJfYnN0ID0gbGdiLnRyYWluKAogICAgcGFyYW1zPWltZGJfbGdiX3BhcmFtcywKICAgIG51bV9ib29zdF9yb3VuZD0xMDAwLCBlYXJseV9zdG9wcGluZ19yb3VuZHM9MjAsCiAgICB0cmFpbl9zZXQ9aW1kYl9YX3RyLCB2YWxpZF9zZXRzPVtpbWRiX1hfdGVdLAogICAgdmVyYm9zZV9ldmFsPTEwMCkKICBfID0gaW1kYl9ic3Quc2F2ZV9tb2RlbChpbWRiX2xnYl9tb2RlbF9maWxlKQoKaW1kYl9sZ2JfeWhhdCA9IGltZGJfYnN0LnByZWRpY3QoaW1kYl9YX3Rlc3QpCmltZGJfbGdiX3ByZWQgPSAoaW1kYl9sZ2JfeWhhdCA+IC41KS5hc3R5cGUoaW50KQoKcHJpbnQoY2xhc3NpZmljYXRpb25fcmVwb3J0KGltZGJfeV90ZXN0LCBpbWRiX2xnYl9wcmVkKSkKcHJpbnQocm9jX2F1Y19zY29yZShpbWRiX3lfdGVzdCwgaW1kYl9sZ2JfeWhhdCkpCmBgYAoKQmFzZWQgb24gdGhlIHRlc3RpbmcgQVVDIHNjb3JlIHdlIGZpbmQgb3V0IHRoYXQgR0JUIHBlcmZvcm1zIGNvbXBhcmFibHkgdG8gbmV1cmFsIG5ldHdvcmsgbW9kZWxzLgoKSnVzdCBsaWtlIFJGIHdlIHdpbGwgaGF2ZSBhY2Nlc3MgdG8gdGhlIG92ZXJhbGwgZmVhdHVyZSBpbXBvcnRhbmNlIHdpdGggYSBHQlQgbW9kZWw6Cl5bQnkgZGVmYXVsdCBgbGlnaHRnYm1gIGNhbGN1bGF0ZXMgdGhlIGltcG9ydGFuY2UgYnkgY291bnRpbmcgaG93IG1hbnkgdGltZXMgYSBmZWF0dXJlIGNvbnRyaWJ1dGVzIHRvIGFuIG9wdGltYWwgc3BsaXQgZHVyaW5nIHRyYWluaW5nLgpJdCBhbHNvIHN1cHBvcnRzIHRoZSBpbXB1cml0eS1iYXNlZCBhcHByb2FjaCB3aXRoIGFyZ3VtZW50IGBpbXBvcnRhbmNlX3R5cGVgIHNldCB0byBgImdhaW4iYC5dCgpgYGB7cHl0aG9uIGltZGJfbGdiX2ZlYXRfaW1wfQpheCA9IGxnYi5wbG90X2ltcG9ydGFuY2UoaW1kYl9ic3QsIG1heF9udW1fZmVhdHVyZXM9MjApCnBsdC5zaG93KCkKYGBgCgpUaGUgZ2xvYmFsIGZlYXR1cmUgcmFua2luZyByZXZlYWxzIHNvbWUgaGlnaGx5IHJhbmtlZCBmZWF0dXJlcyB0byBiZSBtZWFuaW5nbGVzcyBvbiBpdHMgb3duLgpFc3BlY2lhbGx5IHRoZSB3b3JkIGBpdGAuCkJ1dCBhcyBkaXNjdXNzZWQgZWFybGllciB3ZSBzaG91bGRuJ3Qgb3Zlci1pbnRlcnByZXQgdGhlIHJhbmtzIHdpdGhvdXQgYSBwcm9wZXIgZXhwbGFuYXRpb24gbW9kZWxpbmcuCgpTaW5jZSBgc2hhcC5UcmVlRXhwbGFpbmVyYCBpcyBjdXN0b21pemVkIGZvciBHQlQgZm9yIHNwZWVkLAp3ZSBjYW4gZmVlZCBpbiBhbGwgdGVzdGluZyBleGFtcGxlcyB0byBjYWxjdWxhdGUgYWxsIHNoYXAgdmFsdWVzIGF0IG9uY2UuCgpgYGB7cHl0aG9uIHNoYXBfaW1kYl9sZ2J9CmltcG9ydCBzaGFwCgojIFNwYXJzZSBtYXRyaXggaXMgc3VwcG9ydGVkIGJ5IHNoYXAgZm9yIGxpZ2h0Z2JtIG1vZGVscy4KaW1kYl9sZ2JfZXhwbGFpbmVyID0gc2hhcC5UcmVlRXhwbGFpbmVyKGltZGJfYnN0KQppbWRiX2xnYl9zaGFwX3ZhbHVlcyA9IGltZGJfbGdiX2V4cGxhaW5lci5zaGFwX3ZhbHVlcyhpbWRiX1hfdGVzdCkKCmRlZiBpbWRiX2xnYl9zaGFwX3Bsb3QodGVzdF9pZCwgbWF0cGxvdGxpYj1UcnVlKToKICBzaGFwX3BsdCA9IHNoYXAuZm9yY2VfcGxvdCgKICAgIGltZGJfbGdiX2V4cGxhaW5lci5leHBlY3RlZF92YWx1ZVsxXSwKICAgIGltZGJfbGdiX3NoYXBfdmFsdWVzWzFdW3Rlc3RfaWQsOl0sCiAgICBpbWRiX1hfdGVzdFt0ZXN0X2lkLDpdLnRvYXJyYXkoKSwgICMgV2Ugc3RpbGwgbmVlZCBhIGRlbnNlIG1hdHJpeCBoZXJlLgogICAgZmVhdHVyZV9uYW1lcz1zb3J0ZWRfdm9jYWIsCiAgICBtYXRwbG90bGliPW1hdHBsb3RsaWIKICApCiAgcmV0dXJuIHNoYXBfcGx0CmBgYAoKIyMjIyMgR2xvYmFsIEltcG9ydGFuY2Ugey19CgpPbmUgYWR2YW50YWdlIG9mIGBzaGFwYCBvbiBHQlQgbW9kZWxzIGlzIHRoZSBjYXBhYmlsaXR5IG9mIHRyYXZlcnNlIHRocm91Z2ggYWxsIHRoZSB0ZXN0aW5nIGV4YW1wbGVzIGR1ZSB0byBpdHMgZWZmaWNpZW5jeS4KU28gd2UgY2FuIGJhc2VkIG9uIGFsbCB0aGUgcmVzdWx0aW5nIHNoYXAgdmFsdWVzIHRvIGRlcml2ZSBhIGdsb2JhbCBmZWF0dXJlIGltcG9ydGFuY2UganVkZ2VkIGJ5IHRoZWlyIGF2ZXJhZ2Ugc2hhcCB2YWx1ZXMgKGNvbnRyaWJ1dGlvbnMpLgpOb3RlIHRoYXQgdGhpcyBpcyBkaWZmZXJlbnQgZnJvbSB0aGUgbG9zcy9pbXB1cml0eSBvciBzcGxpdCB0aW1lLWJhc2VkIGZlYXR1cmUgcmFua2luZyBkZXJpdmVkIGZyb20gUkYvR0JUICpkdXJpbmcgdHJhaW5pbmcqLgpJdCBpcyBhbiBhZ2dyZWdhdGlvbiBmcm9tIGFsbCBsb2NhbCBwcmVkaWN0aW9uIGV4cGxhbmF0aW9ucyAoY29udHJpYnV0aW9ucykgKmR1cmluZyB0ZXN0aW5nIGRhdGEgaW5mZXJlbmNlKi4KCmBgYHtweXRob24gc2hhcF9pbWRiX2xnYl9mZWF0X2ltcH0Kc2hhcC5zdW1tYXJ5X3Bsb3QoaW1kYl9sZ2Jfc2hhcF92YWx1ZXMsIGltZGJfWF90ZXN0LCBmZWF0dXJlX25hbWVzPXNvcnRlZF92b2NhYiwKICAgICAgICAgICAgICAgICAgcGxvdF90eXBlPSJiYXIiLCBtYXhfZGlzcGxheT0yMCwgc2hvdz1GYWxzZSwgcGxvdF9zaXplPS4yNSkKcGx0LnNob3coKQpgYGAKCkFzIHdlIGNhbiBzZWUsCnRoZSByYW5raW5nIGJhc2VkIG9uIHNoYXAgdmFsdWVzIGZvciB0ZXN0aW5nIHNldCB3aWxsIGJlIGluIGdlbmVyYWwgZGlmZmVyZW50IGZyb20gdGhlIHJhbmtpbmcgYmFzZWQgb24gdHJhaW5pbmcgc3BsaXQuCkFuZCBpdCBpcyBtb3JlICppbnRlcnByZXRhYmxlKjoKRmVhdHVyZXMgd2l0aCBoaWdoZXIgcmFuayBsaXRlcmFsbHkgaGF2ZSBhdmVyYWdlbHkgaGlnaGVyIGltcGFjdCBvbiB0aGUgdGVzdGluZyBkYXRhc2V0LgpBbHNvIHRoZSByYW5raW5nIGNhbiBiZSBjb25kaXRpb25lZCBvbiBsYWJlbHMuCgojIyMjIyBMb2NhbCBFeHBsYW5hdGlvbiB7LX0KClRoZSBtb3N0IGltcG9ydGFudCBhcHBsaWNhdGlvbiBvZiBgc2hhcGAgc3RpbGwgbGllcyBvbiBpbnN0YW5jZS1sZXZlbCBleHBsYW5hdGlvbi4KV2Ugc3RpY2sgdG8gdGhlIHByZXZpb3VzIHR3byByZXZpZXdzLgpGb3IgdGhlIHJldmlldyB0aGF0IFJGIGNvcnJlY3RseSBsYWJlbCBwb3NpdGl2ZSwKd2UgaGF2ZSB0aGUgYHNoYXBgIGV4cGxhbmF0aW9uIHdpdGggdGhlIGZvbGxvd2luZyB2aXN1YWxpemF0aW9uOgoKYGBge3B5dGhvbiBzaGFwX2ltZGJfbGdiX3RwX2V4cH0KaW1kYl9sZ2Jfc2hhcF9wbG90KGltZGJfcmZfdHBfaWR4WzBdKQpgYGAKCk5vdGUgdGhhdCBieSBkZWZhdWx0IGBzaGFwYCBmb3IgYGxpZ2h0Z2JtYCBzaG93cyBsb2ctb2RkcyByYXRoZXIgdGhhbiBwcm9iYWJpbGl0eSBpbiB0aGUgcGxvdC4KU28gYSBwb3NpdGl2ZSB2YWx1ZSBpbmRpY2F0ZXMgYSBwb3NpdGl2ZSBwcmVkaWN0aW9uLApvdGhlcndpc2UgbmVnYXRpdmUuCgpUbyB2ZXJpZnkgdGhpczoKCmBgYHtweXRob24gdmVyaWZ5X2xvZ19vZGRzfQpkZWYgdG9fbG9nX29kZHMocCk6CiAgcmV0dXJuIG5wLmxvZyhwIC8gKDEgLSBwKSkKCmRlZiB0b19wKGxvZ19vZGRzKToKICByZXR1cm4gbnAuZXhwKGxvZ19vZGRzKS8oMSArIG5wLmV4cChsb2dfb2RkcykpCgojIFRha2UgdGhlIGZpcnN0IHRydWUgcG9zaXRpdmUgdG8gZXhhbWluZToKcCA9IGltZGJfYnN0LnByZWRpY3QoaW1kYl9YX3Rlc3RbaW1kYl9yZl90cF9pZHhbMF0sOl0udG9hcnJheSgpKQpwcmludChwKQpwcmludCh0b19sb2dfb2RkcyhwKSkgICMgVGhpcyBpcyB0aGUgcmVwb3J0ZWQgbnVtYmVyIG9uIHRoZSBkZWZhdWx0IHNoYXAgcGxvdC4KYGBgCgpGb3IgYW55IGdpdmVuIHByZWRpY3Rpb24sCnRoZSBzaGFwIHZhbHVlcyBvZiBhbGwgZmVhdHVyZXMgc2hvdWxkIHN1bSB1cCB0byB0aGUgZGlmZmVyZW5jZSBiZXR3ZWVuIHRoZSBwcmVkaWN0ZWQgbG9nLW9kZHMgYW5kIHRoZSBleHBlY3RlZCBsb2ctb2Rkcy4KVG8gdmVyaWZ5IHRoaXMgb24gdGhlIHNwZWNpZmljIHBvc2l0aXZlIGV4YW1wbGU6CgpgYGB7cHl0aG9uIHZlcmlmeV9zaGFwX3ZhbHVlc30KZXhwZWN0ZWRfbG9nX29kZHMgPSBpbWRiX2xnYl9leHBsYWluZXIuZXhwZWN0ZWRfdmFsdWVbMV0KcHJlZGljdGVkX2xvZ19vZGRzID0gdG9fbG9nX29kZHMocCkKCnByaW50KHByZWRpY3RlZF9sb2dfb2RkcyAtIGV4cGVjdGVkX2xvZ19vZGRzKSAgIyBUaGUgZGlmZmVyZW5jZS4KCnNoYXBfdiA9IHBkLkRhdGFGcmFtZSh7CiAgInRva2VuIjogc29ydGVkX3ZvY2FiLAogICJzaGFwX3ZhbHVlIjogaW1kYl9sZ2Jfc2hhcF92YWx1ZXNbMV1baW1kYl9yZl90cF9pZHhbMF0sOl0sCiAgInRmaWRmIjogbnAuc3F1ZWV6ZShpbWRiX1hfdGVzdFtpbWRiX3JmX3RwX2lkeFswXV0udG9hcnJheSgpKQp9KQpzaGFwX3YgPSBzaGFwX3Yuc29ydF92YWx1ZXMoInNoYXBfdmFsdWUiLCBhc2NlbmRpbmc9RmFsc2UpCnByaW50KHNoYXBfdikgICMgU2hhcCB2YWx1ZXMgb2YgYWxsIGZlYXR1cmVzIGZvciB0aGUgZXhhbXBsZS4KCnByaW50KHNoYXBfdi5zaGFwX3ZhbHVlLnN1bSgpKSAgIyBUaGUgc3VtIG9mIHNoYXAgdmFsdWVzLgpgYGAKCkZyb20gdGhlIGVudGlyZSBzaGFwIHZhbHVlcyB3ZSBjYW4ga25vdyBmb3IgZXhhbXBsZSB0aGF0IHRoZSBhYnNlbmNlIG9mIGBncmVhdGAgY29udHJpYnV0ZXMgbmVnYXRpdmVseSwKYW5kIHRoZSBwcmVzZW5jZSBvZiBgbG92ZWAgY29udHJpYnV0ZXMgcG9zaXRpdmVseSwKdG8gdGhlIGZpbmFsIHByZWRpY3Rpb24uCgpgYGB7cHl0aG9uIHNoYXBfaW1kYl9sZ2JfZnBfZXhwfQppbWRiX2xnYl9zaGFwX3Bsb3QoaW1kYl9yZl9mcF9pZHhbMF0pCmBgYAoKRm9yIHRoZSBmYWxzZSBwb3NpdGl2ZSBjYXNlLApzaW1pbGFyIHRvIFJGLAp0aGUgd29yZCBgZ3JlYXRgIHBsYXkgYSBiaWcgcm9sZSBpbiBzaGFwaW5nIHRoZSBHQlQgcHJlZGljdGlvbiB0b3dhcmQgcG9zaXRpdmUuCgojIyMjIEV4cGxhaW4gTmV1cmFsIE5ldHMgd2l0aCBXb3JkIEVtYmVkZGluZ3Mgey19CgpBcyBvZiBgciBmb3JtYXQoU3lzLnRpbWUoKSwgJyVZLSVtLSVkJylgIGBzaGFwLkRlZXBFeHBsYWluZXJgIGRvZXMgbm90IHlldCBzdXBwb3J0IFRGIDIuMC5eW2h0dHBzOi8vZ2l0aHViLmNvbS9zbHVuZGJlcmcvc2hhcC9pc3N1ZXMvODUwLl0KQW5kIGBzaGFwLkdyYWRpZW50RXhwbGFpbmVyYCBpcyBub3Qgd2VsbCBkb2N1bWVudGVkIHlldCBmb3IgVEYgMi4wLgpTbyB3ZSB3aWxsIHVzZSB0aGUgYHNoYXAuS2VybmVsRXhwbGFpbmVyYCB3aGljaCBpcyBhIGltcGxlbWVudGF0aW9uLWFnbm9zdGljIGV4cGxhaW5lciBpbiBgc2hhcGAuClRoZSBjb21wcm9taXNlIGlzIHRoYXQgaXQgd2lsbCBydW4gdmVyeSBzbG93IGZvciBlYWNoIHByZWRpY3Rpb24uCgpgYGB7cHl0aG9uIHNoYXBfa2VybmVsX2ltZGJfbm59CmltZGJfZXhwX2luZCA9IG5wLmFycmF5KFtpbWRiX3JmX3RwX2lkeFswXSwgaW1kYl9yZl9mcF9pZHhbMF1dKQojIEtlcm5lbEV4cGxhaW5lci4KZGVmIG1tKFgpOgogIHJldHVybiBpbWRiX3RyLnByZWRpY3RfcHJvYmEoWClbOiwxXQoKaW1kYl9ubl9zaGFwX2V4cGxhaW5lciA9IHNoYXAuS2VybmVsRXhwbGFpbmVyKG1tLCBzZXFfdHJhaW5fcGFkZGVkWzoxMDBdKQojIFRoaXMgaXMgVkVSWSBzbG93Li4uCiNpbWRiX25uX2tlcm5lbF9zaGFwX3ZhbHVlcyA9IGltZGJfbm5fc2hhcF9leHBsYWluZXIuc2hhcF92YWx1ZXMoc2VxX3Rlc3RfcGFkZGVkW2ltZGJfZXhwX2luZF0pCiMgVE9ETzoKIyBDb250cmlidXRpb24gaXMgYXR0cmlidXRlZCB0byBvcmlnaW5hbCBzZXF1ZW5jZSBpbnB1dC4KIyBJbiBvcmRlciB0byBtYWtlIGV4cGxhbmF0aW9uIHJlYWRhYmxlLAojIHdlIG5lZWQgdG8gbWFwIGVhY2ggcG9zaXRpb24gdG8gb3JpZ2luYWwgd29yZCBpZCB0aGVuIHRvIHdvcmQuCmBgYAoKYGBgcHl0aG9uCiMgVE9ETzogTWFramUgc3VyZSBldmVyeXRoaW5nIHdvcmtzIGhlcmUuCgojIHNoYXAgZG9lcyBub3Qgc3VwcG9ydCBrZXJhcyBtb2RlbCBpbiBzY2lraXQtbGVhcm4gd3JhcHBlci4KIyBMZXQncyByZS1idWlsZCB0aGUgbW9kZWwgYW5kIHJldGFpbiBpdHMgU2VxdWVudGFsIGNsYXNzLgpkbF9tb2RlbCA9IG1vZGVsX2ZuKCkKbWV0cmljcyA9IGRsX21vZGVsLmZpdCgKICB4PXNlcV90cmFpbl9wYWRkZWQsIHk9aW1kYl95X3RyYWluLAogIGJhdGNoX3NpemU9MjU2LCBlcG9jaHM9MjAsCiAgdmFsaWRhdGlvbl9kYXRhPShzZXFfdGVzdF9wYWRkZWQsIGltZGJfeV90ZXN0KSwKICB2YWxpZGF0aW9uX3N0ZXBzPTIwLAogIGNhbGxiYWNrcz1bCiAgICB0Zi5rZXJhcy5jYWxsYmFja3MuRWFybHlTdG9wcGluZyhtb25pdG9yPSJ2YWxfbG9zcyIsIHBhdGllbmNlPTIpLAogICAgdGYua2VyYXMuY2FsbGJhY2tzLk1vZGVsQ2hlY2twb2ludCh0cl9tb2RlbF9maWxlLCBtb25pdG9yPSJ2YWxfbG9zcyIsIHNhdmVfYmVzdF9vbmx5PVRydWUpCiAgXSwKICB2ZXJib3NlPTApCgojIERlZXBFeHBsYWluZXIuCmRsX3NoYXBfZXhwbGFpbmVyID0gc2hhcC5EZWVwRXhwbGFpbmVyKGRsX21vZGVsLCBzZXFfdHJhaW5fcGFkZGVkKSAgIyBXb250JyB3b3JrLgoKIyBHcmFkaWVudEV4cGxhaW5lci4KaW1kYl9ubl9zaGFwX2V4cGxhaW5lciA9IHNoYXAuR3JhZGllbnRFeHBsYWluZXIoZGxfbW9kZWwsIHNlcV90cmFpbl9wYWRkZWRbOjEwMF0pCgppbWRiX25uX3NoYXBfZXhwbGFpbmVyID0gc2hhcC5HcmFkaWVudEV4cGxhaW5lcigKICAoaW1kYl90ci5sYXllcnNbMF0uaW5wdXQsIGltZGJfdHIubGF5ZXJzWy0xXS5vdXRwdXQpLCAgIyBOb3Qgd29ya2luZyBmb3IgVEYgMi4wLgogIHNlcV90cmFpbl9wYWRkZWRbOjEwMF0pCmltZGJfbm5fc2hhcF9leHBsYWluZXIuc2hhcF92YWx1ZXMoc2VxX3Rlc3RfcGFkZGVkWzozXSkgICMgRXJyb3IgaGVyZS4KYGBgCgojIyMgT24gVGFidWxhciBEYXRhIENsYXNzaWZpZXJzCgpXZSBkbyB0aGUgc2FtZSBleGVyY2lzZSBvbiB0aGUgdGFidWxhciBkYXRhc2V0IHByZXZpb3VzbHkgZXhwbGFpbmVkIGJ5IGBsaW1lYC4KCiMjIyMgRXhwbGFpbiBSYW5kb20gRm9yZXN0IHstfQoKYGBge3B5dGhvbiBzaGFwX3VjaWhkX3JmfQp1Y2loZF9yZl9leHBsYWluZXIgPSBzaGFwLlRyZWVFeHBsYWluZXIodWNpaGRfcmYpCnVjaWhkX3JmX3NoYXBfdmFsdWVzID0gdWNpaGRfcmZfZXhwbGFpbmVyLnNoYXBfdmFsdWVzKHVjaWhkX1hfdGVzdCkKCmRlZiB1Y2loZF9yZl9zaGFwX3Bsb3QodGVzdF9pZCwgbWF0cGxvdGxpYj1UcnVlKToKICBzaGFwX3BsdCA9IHNoYXAuZm9yY2VfcGxvdCgKICAgIHVjaWhkX3JmX2V4cGxhaW5lci5leHBlY3RlZF92YWx1ZVsxXSwKICAgIHVjaWhkX3JmX3NoYXBfdmFsdWVzWzFdW3Rlc3RfaWQsOl0sCiAgICB1Y2loZF9YX3Rlc3QuaWxvY1tbdGVzdF9pZF1dLAogICAgbWF0cGxvdGxpYj1tYXRwbG90bGliCiAgKQogIHJldHVybiBzaGFwX3BsdApgYGAKCiMjIyMjIEdsb2JhbCBGZWF0dXJlIEltcG9ydGFuY2Ugey19CgpGcm9tIHRoZSBnbG9iYWwgcmFua2luZyB3ZSBjYW4gY29uZmlybSB0aGF0IHZhcmlhYmxlIGBjYWAgaXMgZGVmaW5pdGVseSBhbiBpbmZsdWVudGlhbCBmZWF0dXJlLgoKIyMjIyMjIFNoYXAgdmFsdWUgZmVhdHVyZSByYW5raW5nIHstfQoKYGBge3B5dGhvbiBzaGFwX3VjaWhkX3JmX2ZlYXRfaW1wfQpzaGFwLnN1bW1hcnlfcGxvdCh1Y2loZF9yZl9zaGFwX3ZhbHVlcywgdWNpaGRfWF90ZXN0LAogICAgICAgICAgICAgICAgICBwbG90X3R5cGU9ImJhciIsIG1heF9kaXNwbGF5PTEwLCBzaG93PUZhbHNlLCBwbG90X3NpemU9LjI1KQpwbHQuZ2NmKCkuc3VicGxvdHNfYWRqdXN0KGJvdHRvbT0uMjUsIGxlZnQ9LjI1KQpwbHQuc2hvdygpCmBgYAoKIyMjIyMjIFNwbGl0LXRpbWUtYmFzZWQgZmVhdHVyZSByYW5raW5nIHstfQoKYGBge3B5dGhvbiB1Y2loZF9yZl9mZWF0X2ltcH0KdWNpaGRfcmZfZmVhdF9pbXAgPSBwZC5TZXJpZXModWNpaGRfcmYuZmVhdHVyZV9pbXBvcnRhbmNlc18sIGluZGV4PXVjaWhkX1hfdHJhaW4uY29sdW1ucykuc29ydF92YWx1ZXMoKQpheCA9IHVjaWhkX3JmX2ZlYXRfaW1wLnRhaWwoMTApLnBsb3Qoa2luZD0iYmFyaCIpCnBsdC5zaG93KCkKYGBgCgojIyMjIyMgRmVhdHVyZSBJbnRlcmFjdGlvbiB7LX0KCldlIGNhbiBwbG90ICpwYXJ0aWFsIGRlcGVuZGVuY3kqIGJhc2VkIG9uIHNoYXAgdmFsdWVzIG9mIHR3byBmZWF0dXJlcyBvdmVyIHRoZSBlbnRpcmUgdGVzdGluZyBkYXRhc2V0LgpGb3IgZXhhbXBsZSwKYnkga25vd2luZyB0aGF0IGBjYWAgaXMgaW1wb3J0YW50LAp3ZSdkIGxpa2UgdG8gZnVydGhlciBrbm93IGhvdyBgYWdlYCBjYW4gaW1wYWN0IHRoZSBjb250cmlidXRpb24gb2YgYGNhYCBhY3Jvc3MgZGlmZmVyZW50IGV4YW1wbGVzLgoKYGBge3B5dGhvbiBzaGFwX3JmX2RlcF9wbG90X2FnZV9jYX0Kc2hhcC5kZXBlbmRlbmNlX3Bsb3QoImFnZSIsIHVjaWhkX3JmX3NoYXBfdmFsdWVzWzFdLCB1Y2loZF9YX3Rlc3QsIGludGVyYWN0aW9uX2luZGV4PSJjYSIsIHNob3c9RmFsc2UpCnBsdC5nY2YoKS5zdWJwbG90c19hZGp1c3QobGVmdD0uMjUpCnBsdC5zaG93KCkKYGBgCgpUaGUgcmVzdWx0IHN1Z2dlc3RzIHR3byB0aGluZ3M6CgoxLiBUaGUgbW9kZWwgd2lsbCBwcmVkaWN0IGhpZ2hlciByaXNrIGZvciBvbGRlciBwZW9wbGUKMi4gYGNhYCBoYXMgbGVzcyBpbXBhY3QgZm9yIHlvbmdlciBwZW9wbGUKCkJvdGggY2FuIGJlIGV4YW1pbmVkIGJ5IGRvbWFpbi1leHBlcnRzIHRvIHNlZSBpZiB0aGUgbW9kZWwgaXMgbGVhcm5pbmcgdGhlIGNvcnJlY3QgcGF0dGVybiB0aGF0IHdlIGV4cGVjdGVkIG9yIGF0IGxlYXN0IHRoYXQgd2UgY2FuIHJlYXNvbi4KCiMjIyMjIExvY2FsIEV4cGxhbmF0aW9uIHstfQoKTm90ZSB0aGF0IGZvciBgc2Npa2l0LWxlYXJuYCBSRiBtb2RlbCBieSBkZWZhdWx0IGBzaGFwYCByZXBvcnRzIHByb2JhYmlsaXR5IGluc3RlYWQgb2YgbG9nLW9kZHMuClN1Y2ggYmVoYXZpb3IgZGlmZmVyZW5jZSByZXN1bHRzIGZyb20gdGhlIG9wdGltaXphdGlvbiBjdXN0b21pemVkIGZvciBHQlQgbW9kZWwgZmFtaWx5LgoKYGBge3B5dGhvbiBzaGFwX3VjaWhkX3JmX3RwX2V4cH0KIyBUaGUgdHJ1ZSBwb3NpdGl2ZSBjYXNlIGluIFJGLgp1Y2loZF9yZl9zaGFwX3Bsb3QodWNpaGRfcmZfdHBfaWR4WzBdKQpgYGAKCmBgYHtweXRob24gc2hhcF91Y2loZF9yZl9mcF9leHB9CiMgVGhlIGZhbHNlIHBvc2l0aXZlIGNhc2UgaW4gUkYuCnVjaWhkX3JmX3NoYXBfcGxvdCh1Y2loZF9yZl9mcF9pZHhbMF0pCmBgYAoKIyMjIyBFeHBsYWluIEdyYWRpZW50IEJvb3N0aW5nIFRyZWVzIHstfQoKRm9yIEdCVCB3ZSBmZWVkIHRoZSBtb2RlbCB0aGF0IGlzIG9wdGltaXplZCwKd2hlcmUgY2F0ZWdvcmljYWxzIGFyZSBlbmNvZGVkIGludGVybmFsbHkgd2l0aG91dCBleHBsaWNpdCBvbmUtaG90IGVuY29kaW5nLgoKYGBge3B5dGhvbiBzaGFwX3VjaWhkX2xnYn0KdWNpaGRfbGdiX2V4cGxhaW5lciA9IHNoYXAuVHJlZUV4cGxhaW5lcih1Y2loZF9ic3RfMikKdWNpaGRfbGdiX3NoYXBfdmFsdWVzID0gdWNpaGRfbGdiX2V4cGxhaW5lci5zaGFwX3ZhbHVlcyh1Y2loZF90ZXN0LmRyb3AoImxhYmVsIiwgYXhpcz0xKSkKCmRlZiB1Y2loZF9sZ2Jfc2hhcF9wbG90KHRlc3RfaWQsIG1hdHBsb3RsaWI9VHJ1ZSk6CiAgc2hhcF9wbHQgPSBzaGFwLmZvcmNlX3Bsb3QoCiAgICB1Y2loZF9sZ2JfZXhwbGFpbmVyLmV4cGVjdGVkX3ZhbHVlWzFdLAogICAgdWNpaGRfbGdiX3NoYXBfdmFsdWVzWzFdW3Rlc3RfaWQsOl0sCiAgICB1Y2loZF90ZXN0Lmlsb2NbW3Rlc3RfaWRdXS5kcm9wKCJsYWJlbCIsIGF4aXM9MSksCiAgICBtYXRwbG90bGliPW1hdHBsb3RsaWIKICApCiAgcmV0dXJuIHNoYXBfcGx0CmBgYAoKIyMjIyMgR2xvYmFsIEZlYXR1cmUgSW1wb3J0YW5jZSB7LX0KCiMjIyMjIyBTcGxpdC10aW1lLWJhc2VkIGZlYXR1cmUgcmFua2luZyB7LX0KCmBgYHtweXRob24gdWNpaGRfbGdiX2ZlYXRfaW1wfQpheCA9IGxnYi5wbG90X2ltcG9ydGFuY2UodWNpaGRfYnN0XzIsIG1heF9udW1fZmVhdHVyZXM9MTApCnBsdC5zaG93KCkKYGBgCgojIyMjIyMgU2hhcCB2YWx1ZSBmZWF0dXJlIHJhbmtpbmcgey19CgpgYGB7cHl0aG9uIHNoYXBfdWNpaGRfbGdiX2ZlYXRfaW1wfQpzaGFwLnN1bW1hcnlfcGxvdCh1Y2loZF9sZ2Jfc2hhcF92YWx1ZXMsIHVjaWhkX3Rlc3QuZHJvcCgibGFiZWwiLCBheGlzPTEpLAogICAgICAgICAgICAgICAgICBwbG90X3R5cGU9ImJhciIsIG1heF9kaXNwbGF5PTEwLCBzaG93PUZhbHNlLCBwbG90X3NpemU9LjI1KQpwbHQuZ2NmKCkuc3VicGxvdHNfYWRqdXN0KGJvdHRvbT0uMjUpCnBsdC5zaG93KCkKYGBgCgojIyMjIyMgRmVhdHVyZSBJbnRlcmFjdGlvbiB7LX0KCmBgYHtweXRob24gc2hhcF9sZ2JfZGVwX3Bsb3RfYWdlX2NhfQpzaGFwLmRlcGVuZGVuY2VfcGxvdCgiYWdlIiwgdWNpaGRfbGdiX3NoYXBfdmFsdWVzWzFdLAogICAgICAgICAgICAgICAgICAgICB1Y2loZF90ZXN0LmRyb3AoImxhYmVsIiwgYXhpcz0xKSwgaW50ZXJhY3Rpb25faW5kZXg9ImNhIiwgc2hvdz1GYWxzZSkKcGx0LmdjZigpLnN1YnBsb3RzX2FkanVzdChsZWZ0PS4yNSkKYGBgCgojIyMjIyBMb2NhbCBFeHBsYW5hdGlvbiB7LX0KCmBgYHtweXRob24gc2hhcF91Y2loZF9sZ2JfdHBfZXhwfQp1Y2loZF9sZ2Jfc2hhcF9wbG90KHVjaWhkX3JmX3RwX2lkeFswXSkKYGBgCgpgYGB7cHl0aG9uIHNoYXBfdWNpaGRfbGdiX2ZwX2V4cH0KdWNpaGRfbGdiX3NoYXBfcGxvdCh1Y2loZF9yZl9mcF9pZHhbMF0pCmBgYAoKIyMjIyBJbXBhY3Qgb2YgT25lLUhvdCBFbmNvZGluZyBPbiBFeHBsYW5hdGlvbiB7LX0KCkFzIG9uZSBtYXkgbm93IHJlYWxpemUsCmJ5IGV4cGxpY2l0bHkgb25lLWhvdC1lbmNvZGUgdGhlIGNhdGVnb3JpY2FsIGZlYXR1cmVzIHdlIGVzc2VudGlhbGx5IHNwbGl0IHRoZW0gaW50byBkaWZmZXJlbnQgZmVhdHVyZXMgaW4gdGhlaXIgaW50ZXJwcmV0YWJsZSByZXByZXNlbnRhdGlvbi4KVGhpcyBjYW4gYmUgZWl0aGVyIGdvb2Qgb3IgYmFkLCBkZXBlbmRpbmcgb24gdGhlIGFjdHVhbCB1c2UgY2FzZS4KRnJvbSB0aGlzIHBhcnRpY3VsYXIgYXNwZWN0IGxpYmFyeSBzdWNoIGFzIGBsaWdodGdibWAgcHJvdmlkZXMgdGhlIGZsZXhpYmlsaXR5IHRvIGFsbG93IHVzIGNob29zZSB3aGV0aGVyIHRvIGRvIHRoZSBvbmUtaG90IGVuY29kaW5nIG9yIG5vdC4KU28gdGhlIHdheSB3ZSB3YW50IHRvIGNvbnN0cnVjdCB0aGUgZXhwbGFuYXRpb24gbW9kZWwgbWF5IHdlbGwgYWZmZWN0IG91ciBpbXBsZW1lbnRhdGlvbiBvZiB0aGUgb3JpZ2luYWwgbW9kZWwuCgojIyMgT24gSW1hZ2UgQ2xhc3NpZmllcnMKCioqVE9ETzogVXNlIGEgcHJlLXRyYWluZWQgbW9kZWw/KioKCiMgRXhwbGFpbmFibGUgQm9vc3RpbmcgTWFjaGluZQoKQG5vcmkyMDE5aW50ZXJwcmV0bWwgcHVibGlzaCB0aGUgb3BlbiBzb3VyY2UgcGFja2FnZSBgaW50ZXJwcmV0YCBmb3IgYSBmYXN0IGltcGxlbWVudGF0aW9uIG9mICoqR2VuZXJhbGl6ZWQgQWRkaXRpdmUgTW9kZWxzIHdpdGggUGFpcndpc2UgSW50ZXJhY3Rpb25zLCBvciBHQTxzdXA+Mjwvc3VwPk0qKiAoQGxvdTIwMTNhY2N1cmF0ZSkuCkFzIG9mIGByIGZvcm1hdChTeXMudGltZSgpLCAnJVktJW0tJWQnKWAsIGBpbnRlcnByZXRgIGlzIHN0aWxsIGluIGl0cyBhbHBoYSByZWxlYXNlIHdpdGggbGltaXRlZCBkb2N1bWVudGF0aW9uLgpUaGUgbGlicmFyeSBjb250YWlucyB0d28gZ3JvdXBzIG9mIG1vZGVsaW5nIGZyYW1ld29ya3M6CgorIGBnbGFzc2JveGA6IGV4cGxhbmFibGUgbWFjaGluZSBsZWFybmluZyBtb2RlbHMKKyBgYmxhY2tib3hgOiBtYWNoaW5lIGxlYXJuaW5nIGV4cGxhbmF0aW9uIG1vZGVscyAoc3VjaCBhcyBMSU1FIGFuZCBTSEFQKQoKV2UndmUgYWxyZWFkeSBjb3ZlcmVkIHRoZSBtYWluc3RyZWFtIGFwcHJvYWNoIGluIHRoZSBzZWNvbmQgZ3JvdXAsCmkuZS4sCm1vZGVscyB0aGF0IGFwcHJveGltYXRlIChsb2NhbGx5KSB0aGUgb3JpZ2luYWwgbW9kZWwgKHN1cHBvc2VkIHRvIGJlIGEgYmxhY2tib3gpIGZvciBiZXR0ZXIgZXhwbGFpbmFiaWxpdHkuClRoZSBtb3JlIGludGVyZXN0aW5nIHBhcnQgb2YgYGludGVycHJldGAgaXMgdG8gYnJpbmcgYWJvdXQgYW5vdGhlciB0eXBlIG9mIG1vZGVsIHRoYXQgaXMgcmVhZGlseSBpbnRlcnByZXRhYmxlIGZyb20gaXRzIHZlcnkgb3JpZ2luLAphbmQgeWV0IHN0aWxsIGNvbXBldGl0aXZlbHkgYWNjdXJhdGU6CnRoZSAqKkV4cGxhaW5hYmxlIEJvb3N0aW5nIE1hY2hpbmUqKiwgb3IgRUJNLgoKRUJNIGlzIGFuIGFkZGl0aXZlIG1vZGVsIG9mIHRoZSBmb3JtOgoKJCQKZyhFKHkpKSA9IFxiZXRhXzAgKyBcc3VtIGZfaiAoeF9qKSArIFxzdW0gZl97aWp9KHhfaSwgeF9qKSwKJCQKCndoZXJlICRnKFxjZG90KSQgaXMgYSBsaW5rIGZ1bmN0aW9uIChzaWdtb2lkIGZvciBiaW5hcnkgY2xhc3NpZmljYXRpb24sIGZvciBhbiBleGFtcGxlKSwKJGZfaiQgaXMgdGhlICpmZWF0dXJlIGZ1bmN0aW9uKiBmb3IgdGhlICRqJC10aCBmZWF0dXJlLApsZWFybmVkIGJ5IGEgZ3JhZGllbnQgYm9vc3RpbmcgYWxnb3JpdGhtIHdpdGggb25seSB0aGF0IGZlYXR1cmUgYXQgYSB0aW1lIGFuZCBpbiBhIHJvdW5kLXJvYmluIGZhc2hpb24gZm9yIGFsbCBmZWF0dXJlcy4KJGZfe2lqfSQgaXMgYSAqcGFpcndpc2UgaW50ZXJhY3Rpb24qIGZlYXR1cmUgZnVuY3Rpb24gdG8gZnVydGhlciBib29zdCB0aGUgYWNjdXJhY3kgb2YgdGhlIG1vZGVsIHdoaWxlIHJlbWFpbiBpbnRlcnByZXRhYmlsaXR5LgoKVGhlIG1vZGVsIGlzIGludGVycHJldGFibGUgc2luY2UgdGhlIGNvbnRyaWJ1dGlvbiBvZiBhbnkgaW5kaXZpZHVhbCBmZWF0dXJlIGNhbiBiZSBkaXJlY3RseSBxdWFudGlmaWVkIGJ5IHRoZWlyIGNvcnJlc3BvbmRpbmcgZmVhdHVyZSBmdW5jdGlvbiAkZl9qJC4KU3VjaCBleHBsYW5hdGlvbiBjYW4gZXh0ZW5kIHVwIHRvIHBhaXJ3aXNlIGludGVyYWN0aW9uIGlmIHBhaXJ3aXNlIGZlYXR1cmUgZnVuY3Rpb25zIGFyZSBhbHNvIGVzdGltYXRlZC4KCiMjIEZlYXR1cmUgU2hhcGUgRnVuY3Rpb25zCgpUaGUgaW5kaXZpZGlhbCBmZWF0dXJlIGZ1bmN0aW9ucyBhcmUgYWxzbyByZWZlcnJlZCB0byBhcyAqc2hhcGUgZnVuY3Rpb25zKiBpbiBHQU0gbGl0ZXJhdHVyZS4KVGhlIG5hbWUgcHJvYmFibHkgY29tZXMgYWZ0ZXIgdGhlIGZhY3QgdGhhdCB1cG9uIGZpbmlzaGluZyBsZWFybmluZyB0aGUgZnVuY3Rpb24sCndlIGNhbiBwbG90IGl0cyBvdXRwdXQgdmFsdWUgJGZfaih4X2opJCBhZ2FpbnN0IGl0cyBpbnB1dCB2YWx1ZSAkeF9qJCwKZWZmZWN0aXZlbHkgdmlzYXVsaXplIHRoZSBzaGFwZSBvZiBwb3NzaWJsZSBjb250cmlidXRpb25zIG9mIHRoYXQgZmVhdHVyZSBhbG9uZyBpdHMgdmFsdWVzIG92ZXIgYSBkYXRhc2V0LgoKTm90ZSB0aGF0IGFsdGhvdWdoIEdBTSBpcyBsaW5lYXIgZHVlIHRvIGl0cyBhZGRpdGl2aXR5LAplYWNoIGluZGl2aWR1YWwgc2hhcGUgZnVuY3Rpb24gY2FuIGJlIChhbmQgbW9zdGx5IGlzKSBub24tbGluZWFyLgpAbG91MjAxMmludGVsbGlnaWJsZSBoYXMgZG9uZSBhIGNvbXByZWhlbnNpdmUgZXhwZXJpbWVudCBvbiBHQU0gKHdpdGhvdXQgdmFyaWFibGUgaW50ZXJhY3Rpb24pIG92ZXIgc2V2ZXJhbCBkYXRhc2V0cy4KVGhleSBmb3VuZCB0aGF0ICpiYWdnZWQgdHJlZXMqIGFzIHNoYXBlIGZ1bmN0aW9ucyBhbmQgKmdyYWRpZW50IGJvb3N0aW5nKiBhcyBHQU0gbGVhcm5lciBoYXMgdGhlIGJlc3QgYWNjdXJhY3kgb3ZlciBzZXZlcmFsIG90aGVyIGNob2ljZXMuCgpUbyBicmllZmx5IHN1bW1hcml6ZSB0aGUgZ3JhZGllbnQgYm9vc3RpbmcgdHJhaW5pbmcgbG9vcCwKaGVyZSBpcyB0aGUgcHNldWRvIGNvZGU6CgpgYGAKc2V0IHRvdGFsIGl0ZXJhdGlvbiA9IE0Kc2V0IHRvdGFsIGZlYXR1cmUgPSBOCmluaXRpYWxpemUgYWxsIGZfaiB3aXRoIDAKZm9yIG0gaW4gMSB0byBNOgogIGZvciBqIGluIDEgdG8gTjoKICAgIGNhbGN1bGF0ZSByZXNpZHVhbHMgd2l0aCBmdWxsIEdBTSBtb2RlbAogICAgbGVhcm4gZl9qIGFnYWluc3QgcmVzaWR1YWxzIGFuZCB1cGRhdGUgdGhlIGZ1bGwgR0FNIG1vZGVsCmBgYAoKIyMgRkFTVCBQYWlyd2lzZSBJbnRlcmFjdGlvbiBEZXRlY3Rpb24KCkl0IHdpbGwgYmUgaW5mZWFzaWJsZSB0byBpbmNsdWRlIGFsbCBwYWlyd2lzZSBpbnRlcmFjdGlvbiBzaW5jZSB0aGUgbnVtYmVyIG9mIHBvc3NpYmxlIHBhaXJzIGdyb3dzIHF1YWRyYXRpY2FsbHkgd2l0aCB0b3RhbCBudW1iZXIgb2YgZmVhdHVyZXMuCkdBPHN1cD4yPC9zdXA+TSBwcm9wb3NlcyBhbiBlZmZpY2llbnQgd2F5IHRvIHJhbmsgcG90ZW50aWFsbHkgc2lnbmlmaWNhbnQgaW50ZXJhY3Rpb24gcGFpcnMgdG8gbGFyZ2VseSByZWR1Y2UgbnVtYmVyIG9mIGZlYXR1cmUgZnVuY3Rpb25zIHRvIGxlYXJuLgpUaGUgcGFpciByYW5raW5nIG9yIGRldGVjdGluZyBhbGdvcml0aG0gaXMgY2FsbGVkIEZBU1QgaW4gdGhlIG9yaWdpbmFsIHBhcGVyLgoKR2l2ZW4gdGhlIGN1cnJlbnQgYmVzdCBtb2RlbCwKcG90ZW50aWFsIGludGVyYWN0aW9ucyBhcmUgZGV0ZWN0ZWQgb24gbW9kZWwgcmVzaWR1YWwuCkFuZCB0aGUgcmVzaWR1YWwgc3VtIG9mIHNxdWFyZXMgKFJTUykgaXMgdXNlZCBhcyB0aGUgY3JpdGVyaWEgd2hldGhlciB0byBpbmNsdWRlIGFkZGl0aW9uYWwgaW50ZXJhY3Rpb24uCklmIFJTUyBkb2VzIG5vdCByZWR1Y2UgZW5vdWdoLAppdCBpcyBzdWdnZXN0aW5nIHRoZSBhZGRpdGlvbmFsIGludGVyYWN0aW9uIGlzIGRvaW5nIG5vIGJlbmVmaXQgdG8gdGhlIGN1cnJlbnQgYmVzdCBtb2RlbC4KCkluIGEgYml0IG1vcmUgZGV0YWlscywKRkFTVCBjb250YWlucyB0d28gc3RhZ2VzOgoKMS4gW1NUQUdFIDFdIEJ1aWxkIHRoZSBiZXN0IEdBTSB3aXRob3V0IGludGVyYWN0aW9ucywgaS5lLiwgJGcoRSh5KSkgPSBcYmV0YV8wICsgXHN1bSBmX2ogKHhfaikkCjIuIFtTVEFHRSAyXSBGaXggJGZfaiQgZm9yIGFsbCAkaiQsIGl0ZXJhdGl2ZWx5IGJ1aWxkIGludGVyYWN0aW9uIGZ1bmN0aW9ucyAkZl97aWp9JCBiYXNlZCBvbiBSU1MgcmVkdWN0aW9uCgpJbiBzdGFnZSBvbmUgdGhlIHByb2Nlc3Mgb2YgbGVhcm5pbmcgb25lLWRpbWVuc2lvbmFsIGZlYXR1cmUgZnVuY3Rpb25zIGlzIGFsc28gY2FsbGVkICpmZWF0dXJlIHNoYXBpbmcqLgpJbiBzdGFnZSB0d28gd2hlcmUgd2UgZXh0ZW5kIEdBTSB0byBHQTxzdXA+Mjwvc3VwPk0sCiRmX3tpan0kIGlzIGEgYml2YXJpYXRlIHRyZWUgbW9kZWwgd2l0aCBlZmZpY2llbnQgaW1wbGVtZW50YXRpb24gYmFzZWQgb24gY3VtdWxhdGl2ZSBoaXN0b2dyYW1zIG9mIHRoZSB0d28gaW52b2x2aW5nIGZlYXR1cmVzLgpBZGRpdGlvbmFsbHksCmNvbnRpbnVvcyBmZWF0dXJlcyBhcmUgZnVydGhlciBkaXNjcmV0aXplZCBpbnRvIGJpbnMgKHNheSwgMjU2IGJpbnMpIG9mIGVxdWktZnJlcXVlbmN5IHRvIHNwZWVkIHVwIHRoZSB0cmVlIHNwbGl0IHRyYWluaW5nLgpeW1RoaXMgaXMgcmVmZXJyZWQgdG8gYXMgKmhpc3RvZ3JhbSB0cmVlKiBhcHByb2FjaCBhbmQgaXMgYWxzbyBhZG9wdGVkIGluIExpZ2h0R0JNJ3MgaW1wbGVtZW50YXRpb24gb2YgZ3JhZGllbnQgYm9vc3RpbmcgdHJlZXMuCkV4cGVyaW1lbnRzIHN1Z2dlc3Qgc3VjaCBhcHByb2FjaCBncmVhdGx5IHJlZHVjZSB0cmFpbmluZyB0aW1lIHdpdGhvdXQgY29tcHJvbWlzaW5nIG1vZGVsIGFjY3VyYWN5Ll0KCiMjIEdsb2JhbCBGZWF0dXJlIFJhbmtpbmcKCkdBPHN1cD4yPC9zdXA+TSByYW5rIGVhY2ggZmVhdHVyZSBpbXBvcnRhbmNlIChlaXRoZXIgb25lLWRpbWVuc2lvbmFsIG9yIGludGVyYWN0aW9uKSBieSB0aGUgc3RhbmRhcmQgZGV2aWF0aW9uIG9mIHRoZSBmdW5jdGlvbiB2YWx1ZXM6ICRcc3FydHtFKGZfal4yKX0kCk5vdGUgdGhhdCBpbiBhIHJlZHVjZWQgZm9ybSB3aGVyZSBmZWF0dXJlIGZ1bmN0aW9uIGl0c2VsZiBpcyBsaW5lYXI6CiRmX2ooeF9qKSA9IHdfanhfaiQsCmFjY29yZGluZyB0byB0aGUgZm9ybXVsYSB0aGUgd2VpZ2h0IGRpcmVjdGx5IGNvcnJlc3BvbmRzIHRvIHRoZSBpbXBvcnRhbmNlIHZhbHVlOgokXHNxcnR7RShmX2peMil9ID0gd19qJC4KCkluIHBsYWluIHdvcmRzLAp0aGUgaW1wb3J0YW5jZSBzY29yZSBtZWFzdXJlcyBob3cgdm9sYXRpbGUgdGhlIGNvbnRyaWJ1dGlvbiBvZiBhIGdpdmVuIGZlYXR1cmUgaXMgb3ZlciBhIGdpdmVuIHRyYWluaW5nIHNldC4KQSBmZWF0dXJlIHdpdGggaGlnaGVyIHZvbGF0aWxpdHkgZXNzZW50aWFsbHkgcGxheSBhIGJpZ2dlciByb2xlIGluIHNoYXBpbmcgdGhlIGZpbmFsIG1vZGVsIGRlY2lzaW9uLgpIZW5jZSBpdCBpcyAoZ2xvYmFsbHkpIG1vcmUgaW1wb3J0YW50LgoKIyMgSGFuZHMtb24gRXhwbGFuYXRpb24gRGVtbwoKIyMjIE9uIFRleHQgLyBJbWFnZSBEYXRhCgpFQk0gaXMgbm90IGVmZmljaWVudCBmb3IgdGV4dCBkYXRhc2V0LgpEdWUgdG8gdGhlIGFsZ29yaXRobSdzIGRlc2lnbiBpdCB3aWxsIHJ1biB0b28gbG9uZyBmb3IgYmFnLW9mLXdvcmRzIG1vZGVsIHNpbmNlIHRoZXJlIGFyZSB0b28gbWFueSBmZWF0dXJlIGZ1bmN0aW9ucyB0byBlc3RpbWF0ZS4KSWYgd2UgZml0IGEgRUJNIHdpdGggdGhlIG1vdmllIHJldmlldyBkYXRhc2V0IChkZWZpbml0ZWx5IG5vdCBhIGJpZyBvbmUpLAp3ZSB3aWxsIGVuY291bnRlciBPT00gKG91dC1vZi1tZW1vcnkpIGlzc3VlIGV2ZW4gd2l0aG91dCBpbnRlcmFjdGlvbiB0ZXJtcy4KQXMgYSByZXN1bHQsCndlIHdpbGwgc2tpcCB0aGUgZGlzY3Vzc2lvbiBvZiBFQk0gb24gYSB0ZXh0IGNsYXNzaWZpZXIuCihUaGUgc2FtZSByZXN0cmljdGlvbiBhcHBsaWVzIHRvIGltYWdlIGRhdGFzZXQuKQoKIyMjIE9uIFRhYnVsYXIgRGF0YQoKYEV4cGxhaW5hYmxlQm9vc3RpbmdDbGFzc2lmaWVyYCBoYXMgYSBgc2Npa2l0LWxlYXJuYCBmYXNoaW9uIEFQSSBhbmQgaGVuY2UgaXMgc3RyYWlnaHRmb3J3YXJkIHRvIHVzZS4KCmBgYHtweXRob24gdWNpaGRfZWJtLCByZXN1bHRzPSJoaWRlIn0KZnJvbSBpbnRlcnByZXQuZ2xhc3Nib3ggaW1wb3J0IEV4cGxhaW5hYmxlQm9vc3RpbmdDbGFzc2lmaWVyCgp1Y2loZF9lYm0gPSBFeHBsYWluYWJsZUJvb3N0aW5nQ2xhc3NpZmllcigKICBuX2VzdGltYXRvcnM9MTYsIGZlYXR1cmVfbmFtZXM9dWNpaGRfMi5jb2x1bW5zLCBuX2pvYnM9MSkKXyA9IHVjaWhkX2VibS5maXQodWNpaGRfWF90cmFpbiwgdWNpaGRfeV90cmFpbikKCnVjaWhkX2VibV95aGF0ID0gdWNpaGRfZWJtLnByZWRpY3RfcHJvYmEodWNpaGRfWF90ZXN0KVs6LDFdCnVjaWhkX2VibV9wcmVkID0gKHVjaWhkX2VibV95aGF0ID4gLjUpLmFzdHlwZShpbnQpCgpwcmludChjbGFzc2lmaWNhdGlvbl9yZXBvcnQodWNpaGRfeV90ZXN0LCB1Y2loZF9lYm1fcHJlZCkpCnByaW50KHJvY19hdWNfc2NvcmUodWNpaGRfeV90ZXN0LCB1Y2loZF9lYm1feWhhdCkpCmBgYAoKVGhlIG1vZGVsIHBlcmZvcm1zIHZlcnkgd2VsbCBvbiB0aGUgaGVhcnQgZGlzZWFzZSBkYXRhc2V0LApvdXRwZXJmb3JtaW5nIGJvdGggUkYgYW5kIEdCVC4KXltUbyB1c2UgRkFTVCBhbGdvcml0aG0gdG8gZGV0ZWN0IGludGVyYWN0aW9ucyB3ZSBuZWVkIHRvIHBhc3MgZXhwbGljaXRseSBhbiBpbnRlZ2VyIGFyZ3VtZW50IGBpbnRlcmFjdGlvbnNgIHRvIHRoZSBjb25zdHJ1Y3Rvci4KQnkgZGVmYXVsdCBgaW50ZXJhY3Rpb25zPTBgLCBpLmUuLCBubyBpbnRlcmFjdGlvbiBpcyBhY3R1YWxseSBlc3RpbWF0ZWQgYXQgYWxsLgpBZnRlciBzZXZlcmFsIGV4cGVyaW1lbnRzIGl0IHNlZW1zIHRoYXQgaW4gdGhpcyBwYXJ0aWN1bGFyIGNhc2UgaW50ZXJhY3Rpb25zIGRvZXMgbm90IGhlbHAgaW1wcm92ZSB0aGUgbW9kZWwuClNvIHdlIHN0aWNrIHRvIHRoZSBkZWZhdWx0IHNldHRpbmcuXQoKIyMjIyBHbG9iYWwgRXhwbGFuYXRpb24gey19CgpgaW50ZXJwcmV0YCBjb21lcyB3aXRoIGEgcmljaCBzZXQgb2YgdmlzdWFsaXphdGlvbiB0b29scyAod2l0aCBbYHBsb3RseWBdKGh0dHBzOi8vcGxvdC5seS8pIGFzIGl0cyBiYWNrZW5kKS4KTW9kZWwgZXhwbGFuYXRpb24gaXMgZGl2aWRlZCBpbnRvIHR3byBncm91cHM6Cmdsb2JhbCBhbmQgbG9jYWwuCgpGb3IgZ2xvYmFsIGV4cGxhbmF0aW9uLAp3ZSBoYXZlIGFjY2VzcyB0byBib3RoIGdsb2JhbCBmZWF0dXJlIGltcG9ydGFuY2UgYW5kIGEgcGVyLWZlYXR1cmUgZmVhdHVyZSBjb250cmlidXRpb24gc3RhdHMuCgpgYGB7cHl0aG9uIHVjaWhkX2VibV9nbG9iYWxfZXhwbGFpbn0KdWNpaGRfZWJtX2dsb2JhbCA9IHVjaWhkX2VibS5leHBsYWluX2dsb2JhbCgpCiMgQWxsIGZlYXR1cmUgaW5mbzoKcHJpbnQodWNpaGRfZWJtX2dsb2JhbC5zZWxlY3RvcikKYGBgCgpgYGB7cHl0aG9uIHVjaWhkX2VibV9nbG9iYWxfZXhwbGFpbl9wbG90fQojIEdsb2JhbCBmZWF0dXJlIGltcG9ydGFuY2UuCnVjaWhkX2VibV9nbG9iYWwudmlzdWFsaXplKCkud3JpdGVfaHRtbCgiL3RtcC91Y2loZF9lYm1fZmVhdF9pbXAuaHRtbCIsIGluY2x1ZGVfcGxvdGx5anM9RmFsc2UpCgojIEdsb2JhbCBjb250cmlidXRpb24gb24gYWdlLgpmaWQgPSB1Y2loZF9lYm1fZ2xvYmFsLnNlbGVjdG9yLk5hbWUudG9saXN0KCkuaW5kZXgoImFnZSIpCnVjaWhkX2VibV9nbG9iYWwudmlzdWFsaXplKGZpZCkud3JpdGVfaHRtbCgiL3RtcC91Y2loZF9lYm1fYWdlX2ltcC5odG1sIiwgaW5jbHVkZV9wbG90bHlqcz1GYWxzZSkKCiMgR2xvYmFsIGNvbnRyaWJ1dGlvbiBvbiB0cmVzdGJwcy4KZmlkID0gdWNpaGRfZWJtX2dsb2JhbC5zZWxlY3Rvci5OYW1lLnRvbGlzdCgpLmluZGV4KCJ0cmVzdGJwcyIpCnVjaWhkX2VibV9nbG9iYWwudmlzdWFsaXplKGZpZCkud3JpdGVfaHRtbCgiL3RtcC91Y2loZF9lYm1fdHJlc3RicHNfaW1wLmh0bWwiLCBpbmNsdWRlX3Bsb3RseWpzPUZhbHNlKQoKIyBHbG9iYWwgY29udHJpYnV0aW9uIG9uIHNleC4KZmlkID0gdWNpaGRfZWJtX2dsb2JhbC5zZWxlY3Rvci5OYW1lLnRvbGlzdCgpLmluZGV4KCJzZXhfMC4wIikKdWNpaGRfZWJtX2dsb2JhbC52aXN1YWxpemUoZmlkKS53cml0ZV9odG1sKCIvdG1wL3VjaWhkX2VibV9zZXhfaW1wLmh0bWwiLCBpbmNsdWRlX3Bsb3RseWpzPUZhbHNlKQpgYGAKCiMjIyMjIEdsb2JhbCBGZWF0dXJlIEltcG9ydGFuY2Ugey19CgpBcyB3ZSBkaXNjdXNzZWQgZWFybGllciB0aGUgZ2xvYmFsIGZlYXR1cmUgaW1wb3J0YW5jZSBzY29yZSBpcyByZXByZXNlbnRlZCBieSB0aGUgc3RhbmRhcmQgZGV2aWF0aW9uIG9mIHRoZSBmZWF0dXJlIGZ1bmN0aW9uIG91dHB1dC4KRmVhdHVyZXMgbWFya2VkIG1vcmUgaW1wb3J0YW50IG1lYW5zIHRoYXQgdGhleSBhcmUgbW9yZSAiYWN0aXZlIiBpbiBzaGFwaW5nIHRoZSBtb2RlbCBkZWNpc2lvbi4KXltGZWF0dXJlIHNoYXBpbmcgcGxvdCBmb3IgaW50ZXJhY3Rpb24gd2lsbCBiZSBwbG90dGVkIGFzIGEgaGVhdG1hcC4KV2UgZGluZCd0IGRlbW8gdGhhdCBzaW5jZSBpbnRlcmFjdGlvbiBkaWRuJ3QgaGVscCBvdXIgbW9kZWwgYXQgYWxsLl0KCmBgYHtyLCBlY2hvPUZBTFNFfQpodG1sdG9vbHM6OmluY2x1ZGVIVE1MKCIvdG1wL3VjaWhkX2VibV9mZWF0X2ltcC5odG1sIikKYGBgCgojIyMjIyBGZWF0dXJlIFNoYXBpbmc6IEFnZSB7LX0KCmBgYHtyLCBlY2hvPUZBTFNFfQpodG1sdG9vbHM6OmluY2x1ZGVIVE1MKCIvdG1wL3VjaWhkX2VibV9hZ2VfaW1wLmh0bWwiKQpgYGAKCiMjIyMjIEZlYXR1cmUgU2hhcGluZzogUmVzdGluZyBCbG9vZCBQcmVzc3VyZSB7LX0KCmBgYHtyLCBlY2hvPUZBTFNFfQpodG1sdG9vbHM6OmluY2x1ZGVIVE1MKCIvdG1wL3VjaWhkX2VibV90cmVzdGJwc19pbXAuaHRtbCIpCmBgYAoKIyMjIyMgRmVhdHVyZSBTaGFwaW5nOiBHZW5kZXIgKEZlbWFsZSkgey19CgpgYGB7ciwgZWNobz1GQUxTRX0KaHRtbHRvb2xzOjppbmNsdWRlSFRNTCgiL3RtcC91Y2loZF9lYm1fc2V4X2ltcC5odG1sIikKYGBgCgojIyMjIExvY2FsIEV4cGxhbmF0aW9uIHstfQoKTW9yZSBpbXBvcnRhbnRseSwKd2UgbXVzdCBiZSBhYmxlIHRvIGV4cGxhaW4gYSBzcGVjaWZpYyBtb2RlbCBwcmVkaWN0aW9uIGxvY2FsbHkuCkVCTSBpcyBpbmhlcmVudGx5IGFibGUgdG8gZG8gZXhhY3RseSB0aGF0LgpVc2luZyBgaW50ZXJwcmV0YCB0aGlzIGNhbiBiZSBkb25lIGVhc2lseSB3aXRoIGEgY291cGxlIG9mIGxpbmVzOgoKYGBge3B5dGhvbiB1Y2loZF9lYm1fbG9jYWxfZXhwbGFpbn0KIyBFeHBsYWluIHRoZSBzYW1lIGluc3RhbmNlcyBwcmV2aW91c2x5IG9uIFJGLgp1Y2loZF9leHBfaW5kID0gbnAuYXJyYXkoW3VjaWhkX3JmX3RwX2lkeFswXSwgdWNpaGRfcmZfZnBfaWR4WzBdXSkKCiMgV2UgY2FuIGZlZWQgbXVsdGlwbGUgZXhhbXBsZXMgYXQgdGhlIHNhbWUgdGltZS4KdWNpaGRfZWJtX2xvY2FsID0gdWNpaGRfZWJtLmV4cGxhaW5fbG9jYWwoCiAgdWNpaGRfWF90ZXN0Lmlsb2NbdWNpaGRfZXhwX2luZCw6XSwgdWNpaGRfeV90ZXN0W3VjaWhkX2V4cF9pbmRdKQp1Y2loZF9lYm1fbG9jYWwudmlzdWFsaXplKDApLndyaXRlX2h0bWwoIi90bXAvdWNpaGRfZWJtX2V4cF90cC5odG1sIiwgaW5jbHVkZV9wbG90bHlqcz1GYWxzZSkKdWNpaGRfZWJtX2xvY2FsLnZpc3VhbGl6ZSgxKS53cml0ZV9odG1sKCIvdG1wL3VjaWhkX2VibV9leHBfZnAuaHRtbCIsIGluY2x1ZGVfcGxvdGx5anM9RmFsc2UpCmBgYAoKYGBge3IsIGVjaG89RkFMU0V9Cmh0bWx0b29sczo6aW5jbHVkZUhUTUwoIi90bXAvdWNpaGRfZWJtX2V4cF90cC5odG1sIikKYGBgCgpGb3IgdGhlIGZhbHNlIHBvc2l0aXZlIGNhc2UgbWFkZSBieSBib3RoIFJGIGFuZCBHQlQsCkVCTSBpcyBhYmxlIHRvIGNvcnJlY3RseSBwcmVkaWN0IHRoZSBuZWdhdGl2ZSBsYWJlbC4KV2Ugc3RpbGwgc2VlIGEgcG9zaXRpdmUgYGNhYCB2YWx1ZSBjb250cmlidXRlIGEgbG90IHRvd2FyZCBhIHBvc2l0aXZlIHByZWRpY3Rpb24sCndoaWxlIEVCTSBpcyBhYmxlIHRvIGFsc28gcGljayB1cCBzZXZlcmFsIG5lZ2F0aXZlIGZhY3RvcnMgdGhhdCBqb2ludGx5IG5lZ2F0ZSB0aGUgcG9zaXRpdmUgaW1wYWN0LAplbmRpbmcgdXAgd2l0aCBhIGNvcnJlY3QgcHJlZGljdGlvbiB0b3dhcmQgbmVnYXRpdmUuCgpgYGB7ciwgZWNobz1GQUxTRX0KaHRtbHRvb2xzOjppbmNsdWRlSFRNTCgiL3RtcC91Y2loZF9lYm1fZXhwX2ZwLmh0bWwiKQpgYGAKCiMgRnJvbSBFeHBsYW5hdGlvbiB0byBUcnVzdAoKVGhyb3VnaG91dCBhbGwgdGhlIGV4ZXJjaXNlcyBhYm92ZSB3ZSBvbmx5IGRlbW9uc3RyYXRlIGxpbWl0ZWQgbG9jYWwgYWN0dWFsIGV4YW1wbGVzLApzbyBub3RoaW5nIHJlYWxseSBjb25jbHVzaXZlIGhlcmUgYXMgd2hpY2ggbW9kZWwgaXMgbW9yZSByZWFzb25hYmxlIGZvciBlYWNoIHByb2JsZW0gaW4gbWFraW5nIHRoZWlyIGRlY2lzaW9uLgpCdXQgd2l0aCBtb3JlIGludmVzdGlnYXRpb24gdGhlcmUgbWF5IGJlIG1vcmUgaW5zaWdodHMgb24gd2hpY2ggbW9kZWwgY2FuIGJlIHRydXN0ZWQgbW9yZSB0aGFuIHRoZSBvdGhlcnMuCgpXZSBzdW1tYXJpemUgdGhlIGJlbmVmaXQgb2YgZXhwbGFuYXRpb24gbW9kZWxpbmcgaGVyZS4KSW4gZ2VuZXJhbCBpdCBhbGxvd3MgdXMuLi4KCjEuIFRvIHJlYXNvbiB0aGUgbW9kZWwgYmVoYXZpb3IgYXQgYSBzaW5nbGUgaW5zdGFuY2UgbGV2ZWwKMi4gVG8gaW52ZXN0aWdhdGUgdW5yZWFzb25hYmxlIGJlaGF2aW9yIHN1Y2ggdGhhdCB3ZSBjYW4gZnVydGhlciBpbXByb3ZlIHRoZSBvcmlnaW5hbCBtb2RlbCB3aXRoIGZlYXR1cmUgZW5naW5lZXJpbmcKMy4gVG8gZGlmZmVyZW50aWF0ZSBkaWZmZXJlbnQgbW9kZWxzIHdpdGggc2ltaWxhciB0ZXN0aW5nIHNjb3Jlcwo0LiBUbyBidWlsZCB0cnVzdCBvbiBhIG1vZGVsLCBlc3BlY2lhbGx5IGZvciB0aGUgZW5kIHVzZXIsIHRvIGJldHRlciBmb3JtdWxhdGUgdGhlIHN1YnNlcXVlbnQgYWN0aW9uIGl0ZW0KCiMgUmVmZXJlbmNlcwo=